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

.NET Core 2.0 ile Basit Bir Web API Geliştirmek

$
0
0

Merhaba Arkadaşlar,

Günler yoğun geçiyor. Bir süredir sosyal medyadan da uzaktayım. Kendimce sebeplerim var. Ağırlık görev değişikliği sonrası kritik geliştirmeler barındıran işimdeki yoğunluk. Bunun dışında daha çok kitap okuduğumu, telefona neredeyse hiç bakmadığımı(Türkiye ortalamasına göre bir kişi günde 70 kez telefona bakıyormuş-kahrolsun Instagram çağı), Serdar Kuzuloğlu'ndan dünya hallerini daha çok okuduğumu, Gündem Özel'i daha çok izlediğimi(Yazıyı yazdığım günlerdeki şu yayınlarını tavsiye ederim. Hasan Söylemez'i de takip edin kitabını alın derim), okuyup dinlediklerimden kendime küçük küçük notlar çıkarttığımı, daha çok basketbol oynadığımı, işe gittiğim her gün gerek otobüs gerek metorbüs gerek minibüs daha çok sıkıştığımı(tutunmadan seyahat edebilmek dahil) ama Beşiktaş-Üsküdar arası motor hattında nefes alarak huzur bulabildiğim günler geçirdiğimi ifade edebilirim. Kalan zamanlarda eskisi kadar çok olmasa da bir şeyler öğrenmeye gayret ediyorum. Bir süredir de .Net Core tarafında servis geliştirme noktasında neler yapılabileceğini incelemek istiyordum. İşlerden boşluk bulduğum bir sırada Web API nasıl yazılır araştırayım ve yaptığım örneği bloğuma ekleyeyim dedim.

Ortam Hazırlıkları

İlk olarak Microsoft'un ilgili adresinden .Net Core'un son sürümünü indirdim. Çalışmaya başladığım tarih itibariyle 2.0 versiyonu bulunuyordu. Kurulumu Windows 7 işletim sistemi olan bir makinede gerçekleştirdim(Şirket bilgisayarı) Saha Hizmetleri ekibimizin de desteği ile makineye 2.0 sürümünü sorunsuz şekilde yükledim(Malum makinede Local Admin'lik olmayınca) Ardından komut satırını ve Notepad++ uygulamasını açtım. Amacım Visual Studio ailesinin (Code dahil)ürünlerini kullanmadan Web API geliştirmenin temellerini anlamaktı. Hem bu sayede hazır şablonların içeridiği kod parçalarını daha iyi anlayabilirdim. Sonrasında terminal penceresine geçtim ve incelediğim kaynaklardan derlediğim notlara da bakarak ilk komutumu verdim.

dotnet --help

ile dotnet komut satırı aracının nasıl kullanılabileceğini incelemeye çalıştım. .Net Core'un komut satırında proje şablonlarını otomatik olarak hazırlayan new komutunun nasıl kullanılabileceğini görmek için de şu komutu kullandım. Bu sayede dotnet'in popüler ve gerekli build, restore, run gibi komutlarını nasıl kullanabileceğimizi detaylı bir şekilde görebiliriz.

dotnet new --help

Sonrasında .Net Core çalışmaları için açtığım klasörde aşağıdaki komutu vererek Fabrika isimli bir Web API projesi oluşturdum.

dotnet new webapi -o Fabrika

Peki şimdi ne oldu? -o parametresi ile verdiğimiz Fabrika ismi nedeniyle Fabrika adında bir klasör oluştu ve içerisine gerekli tüm proje dosyaları hazır olarak eklendi.

Dikkat edileceği üzere Controllers isimli bir klasör de bulunuyor. Temel olarak Model View Controller desenini kullanmaya hazır bir şablon oluşturulduğunu ifade edebiliriz. Bir başka deyişle kullanıcılardan gelecek REST taleplerini kontrol eden(Controllers) geriye dönecek varlıkları(Models) varsayılan olarak JSON tipinde basacak(View) bir desen söz konusu. Varsayılan olarak Models klasörü yoktu. Bunu kendim ekledim.

Oluşturulan proje yapısından sonra ilk yaptığım şey kaynaklarda da belirtildiği üzere ortamı çalıştırmaktı.

dotnet run

Dikkat edileceği üzere http://localhost:5000 adresinden ayağa kalkan ve istemci taleplerini dinlemeye hazır bir sunucu söz konusu. Tabii direkt bu adrese gidersek bir sonuç alamayız. Çünkü varsayılan olarak gelen bir yönlendirme(Router) sistemi var. Bu adres Controllers klasöründeki Controller tipinden türeyen sınıfa göre şekilleniyor. Hazır şablonla gelen ValuesControllers sınıfının kodlarına baktığımızda Route niteliğinin(attribute) kullanıldığını görürüz. Bu nitelikte ifade edilen api/[Controller] bildirimi talep edebileceğimiz HTTP adresinin şeklini belirler ki bu durumda aşağıdaki gibi olmalıdır.

http://localhost:5000/api/values

Sonuçta örnek olarak konulmuş string dizi içeriği elde edilir.

Elbette varsayılan bir Controller sınıfı söz konusu. ValuesController sınıfının içerisinde yer alan metodlar incelendiğinde HTTP Get, Post, Put ve Delete operasyonları için gerekli hazır fonksiyonların konulduğu görülür. Hangi metodun hangi HTTP talebine cevap vereceğini belirtmek için HttpGet, HttpPost, HttpPut ve HttpDelete niteliklerinden yararlanılmaktadır.

EntityFrameworkCore Paketinin Yüklenmesi

Ben bunun üzerine işin içerisine EntityFrameworkCore'u da katmaya ve klasik ürün listelemesi yapan REST servis örneğini inşa etmeye karar verdim. Tabii ilk bulmam gereken Entity Framework Core sürümünün bu projeye nasıl ekleneceğiydi. Söz konusu kütüphane bir NuGet paketi olarak ele alınabildiğinden projenin kullandığı paketler listesinde tanımlanması yeterli olacaktı. Bu yüzden Fabrika.csproj isimli proje dosyasını açtım ve EntityFrameworkCore paketi için ItemGroup elementi altına bir PackageReference bildirimi ekledim.

<Project Sdk="Microsoft.NET.Sdk.Web"><PropertyGroup><TargetFramework>netcoreapp2.0</TargetFramework></PropertyGroup><ItemGroup><Folder Include="wwwroot\" /></ItemGroup><ItemGroup><PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" /><PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="2.0.0"/></ItemGroup><ItemGroup><DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0" /></ItemGroup></Project>

Bu işlemin ardından aşağıdaki komutu kullanarak Microsoft.EntityFrameworkCore.InMemory paketinin 2.0.0 versiyonunun indirilmesini sağlanır. restore ile bildirimi yapılan tüm paketler çözümlenir ve projenin kullanımı için gerekli indirme işlemleri yapılır.

dotnet restore

Model Sınıflarının Yazılması

EntityFrameworkCore paketi eklendiğine göre gerekli Model içeriklerini yazarak ilerleyebilirdim. Product ve FabrikaContext isimli iki sınıfı Models klasörü içerisine aşağıdaki içeriklerle ekledim.

Product sınıfı

namespace Fabrika.Models
{
	public class Product
	{
		public long Id {get;set;}
		public string Name {get;set;}
		public double UnitPrice {get;set;}
	}
}

FabrikaContext sınıfı

using Microsoft.EntityFrameworkCore;

namespace Fabrika.Models
{
	public class FabrikaContext
		:DbContext
	{
		public DbSet<Product> Products{get;set;}
		public FabrikaContext(DbContextOptions<FabrikaContext> options)
			:base(options)
			{			
			}
	}
}

Product tipik bir POCO(Plain Old C# Object) olarak tasarlanmıştır. FabrikaContext ise DbContext türevli basit bir sınıftır ve içerisinde Product tipini kullanan Products isimli bir DbSet barındırmaktadır. base kullanımı nedeniyle varsayılan bir nesne oluşumu söz konusudur.

Controller Sınıfının Yazılması

Model içerikleri de hazır olduğuna göre, istemciden gelecek HTTP talebine göre devreye girecek kontrolcüyü(Controller) yazarak ilerleyebilirim. Bu amaçla Controllers klasörüne ProductsController isimli aşağıdaki içeriğe sahip sınıfı ekledim. Kontrolcünün görevi istemciden gelecek talebi ele alıp modelden yararlanarak bir çıktı üretmekten ibaret.

ProductsController sınıfı

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

namespace Fabrika.Controllers
{
    [Route("Fabrika/restapi/[controller]")]
    public class ProductsController : Controller
    {
		private readonly FabrikaContext _context;
		
		public ProductsController(FabrikaContext context)
		{
			_context=context;
			
			if(_context.Products.Count()==0)
			{
				_context.Products.Add(new Product{Id=19201,Name="Lego Nexo Knights King I",UnitPrice=45});
				_context.Products.Add(new Product{Id=23942,Name="Lego Starwars Minifigure Jedi",UnitPrice=55});
				_context.Products.Add(new Product{Id=30021,Name="Star Wars çay takımı ",UnitPrice=35.50});
				_context.Products.Add(new Product{Id=30492,Name="Star Wars kahve takımı",UnitPrice=24.40});
				
				_context.SaveChanges();
			}
		}
        
        [HttpGet]
        public IEnumerable<Product> Get()
        {
			return _context.Products.ToList();
        }

        
        [HttpGet("{id}")]
        public IActionResult Get(int id)
        {
			var product=_context.Products.FirstOrDefault(t=>t.Id==id);
			if(product==null)
			{
				return NotFound();
			}
			return new ObjectResult(product);
        }

        [HttpPost]
        public void Post([FromBody]string value)
        {
			//TODO:Yazılacak
        }

        [HttpPut("{id}")]
        public void Put(int id, [FromBody]string value)
        {
			//TODO:Yazılacak
        }

        [HttpDelete("{id}")]
        public void Delete(int id)
        {
			//TODO:Yazılacak
        }
    }
}

ProductsController sınıfında route adresinin değiştirildiğinde, DbContext türevli FabrikaContext tipinin kullanıldığında dikkat edelim. Get taleplerini karşılayan iki metodumuz bulunuyor. Birisi tüm ürün listesini döndürmekte. Bu nedenle generi IEnumerable tipini döndürmekte. Diğer Get metodu ise belli bir Id'ye ait ürünü döndürüyor. Bu dönüş için IActionResult arayüzünün taşıyabileceği bir nesne örneği kullanılmakta(ObjectResult) Yapıcı metod içerisinde ürün olmama ihtimaline karşın bir kaç tane örnek ürün eklenmekte. Eklenen ürünler SaveChanges ile veritabanına kayıt altına da alınmakta(Henüz Post, Put ve Delete metodlarını tamamlamadım. Bu fonksiyonlar sonraki boşluk için kendime atadığım görevler)

İlk Deneme

Hemen

dotnet build

komutu ile kodu derledim. Hatasız olduğunu görünce de sevindim ve

dotnet run

ile sunucuyu başlatıp ürünler için tarayıcıdan bir talep girdim.

http://localhost:5000/Fabrika/restapi/products

Ancak çalışma zamanı hataları ile karşılaştım.

FabrikaContext tipi için gerekli servis çözümlemesi bir şekilde yapılamıyordu. Sonrasında DbContext tipini servis olarak eklemeyi unuttuğumu fark ettim. Startup.cs dosyasını açarak ConfigureServices metoduna aşağıdaki satırı ilave etmek sorunun çözümü için yeterliydi(Fabrika.Models ve Microsoft.EntityFrameworkCore namespace bildirimlerini de aldığım hatalar sonrası eklemem gerektiğini itiraf etmek isterim. Biraz daha dikkatli ol Burak!)

public void ConfigureServices(IServiceCollection services)
{
	services.AddDbContext<FabrikaContext>(opt=>opt.UseInMemoryDatabase("FabrikaDb"));
	services.AddMvc();
}

Burada bellekte çalışacak şekilde FabrikaDb isimli bir veritabanını, uygulamanın kullanacağı servisler listesine eklemiş oluyoruz. Örneği tekrar çalıştırdığımda sorun yaşamadım ve tarayıcıdan yaptığım bazı taleplere karşılık aşağıdaki ekran görüntülerinde yer alan sonuçları elde ettiğimi gördüm.

http://localhost:5000/Fabrika/restApi/products talebi sonrası

http://localhost:5000/fabrika/restapi/products/30021 talebi sonrası

http://localhost:5000/fabrika/restapi/products/999 talebi sonrası

--Derken--

Derken Post işlemini de en iyi kaynaklardan birisi olan şuradanöğrenip araya sıkıştırayım dedim ve bu paragraf açılmış oldu.

[HttpPost]
public IActionResult Post([FromBody]Product newProduct)
{
	if(newProduct==null)
		return BadRequest();

	_context.Products.Add(newProduct);
	_context.SaveChanges();
	return CreatedAtRoute("GetProduct",new {id=newProduct.Id},newProduct);				
}

İlk olarak metodun IActionResult döndürecek şekilde değiştirildiğini belirtelim. FromBody niteliği ürün bilgisinin HTTP talebinin Body kısmından okunacağını belirtmekte. Eğer bir ürün bilgisi gelmezse BadRequest mesajı basılıyor. Ürünün veritabanına eklenmesi işi standart bir Entity Framework işi. CreatedAtRoute fonksiyonu HTTP 201 mesajının basılmasını sağlarken aynı zamanda GetProduct isimli bir metoda talepte bulunuyor. Tahmin edeceğiniz üzere yeni eklenen ürünün id bilgisini kullanarak bir HTTP Get talebi yapmakta. Önemli olan kısım ilk parametredeki adın nerede tanımlandığı. Bunu anlayana kadar bir kaç hata aldım da...

[HttpGet("{id}",Name="GetProduct")]
public IActionResult Get(int id)

Name niteliğine atanan değer CreateAtRoute'un kullandığı fonksiyon adı. Böylece istemciye hem işlemin başarılı olduğunu söylüyor hem de yeni oluşan ürün içeriğini gönderiyoruz. Tabii senaryoyu test etmenin en pratik yolu Postman gibi bir araçtan yararlanarak JSON tipinden bir talep göndermek. Aynen aşağıdaki ekran görüntülerinde olduğu gibi.

--Derken--

Varsayılan Port Bilgisinin Değiştirilmesi

Merak ettiğim konulardan birisi de 5000 nolu port bilgisini nasıl değiştirebileceğimdi. Bunun için Program.cs dosyasına uğramak gerekiyor. BuildWebHost fonksiyonunda ortamla ilişkili bir takım ayarlamalar yapılabilir. Örneğin 5555 nolu portun kullanılacağı bilgisi ifade edilebilir. UseUrls fonksiyonuna dikkat edelim.

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

Bu arada Fluent bir metod zinciri söz konusu olduğunu ifade edelim (Bilmeyenler Fluent API nasıl yazılır, Fluent Interface nedir gibi sorularla bir araştırma yapsınlar derim. Buradaki pek çok projemizde bu tip Fluent yapılar kullanıyoruz)

Statik İçeriklere İzin Verilmesi

Merak ettiğim bir diğer konu da hazır olarak gelen wwwroot klasörünü hangi amaçlarla kullanabileceğimizdi. Araştırmalarım sonucunda burada static sayfalara yer verebileceğimizi öğrendim ve şöyle bir HTML sayfası ekledim.

<html><body>
	Fabrika'da üretilen <b><a href="http://localhost:5555/fabrika/restapi/products">ürünler</a></b>.</body></html>

Ne varki sayfaya bir türlü erişemedim. Sonrasında statik dosyaları kullanacağımı çalışma zamanına bildirmem gerektiğini öğrendim. Bunun için startup.cs içerisindeki Configure metodunda UseStaticFiles fonksiyon bildirimini yapmak yeterli.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
	if (env.IsDevelopment())
	{
		app.UseDeveloperExceptionPage();
	}

	app.UseStaticFiles();
	app.UseMvc();
}

Sonrasında index.html sayfasının geldiğini de gördüm.

Bu arada varsayılan olarak wwwroot olarak tanımlanan klasör bilgisini UseWebRoot metodunu kullanarak farklı bir konuma da yönlendirebiliriz(Static sayfaların kullanımı ile ilgili daha fazla detay da var. Şu adrese bakmanızı öneririm)

Swagger Tabanlı Yardım Sayfasının Eklenmesi

Swagger altyapısını baz alan Swashbuckle isimli NuGet paketini kullanarak etkileyici görünüme sahip yardım sayfaları oluşturabiliriz. Böylece API'nin versiyonu, ne tür operasyonlar içerdiği, nasıl kullanıldığı hakkında bilgiler verebilir hatta o an örnek test verileri ile denemeler yaptırtabiliriz. Bunu denemek için ilk olarak komut satırından Fabrika isimli projeye ilgili paketi aşağıdaki ifadeyle ekledim.

dotnet add Fabrika.csproj package Swashbuckle.AspNetCore

Komutu çalıştırdıktan sonra proje dosyasına yeni bir PackageReference bildirimi eklendiğini görebiliriz(Bu arada bir paketi manuel olarak proje dosyasına ekleyip dotnet restore komutu ile ilerlemek yerine bu şekilde işlem yapılabileceğini de öğrenmiş bulundum. Mutluyum)

<ItemGroup><PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" /><PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="2.0.0" /><PackageReference Include="Swashbuckle.AspNetCore" Version="1.0.0" /></ItemGroup>

İlgili servisi kullanıma sunmak içinse Startup.cs sınıfındaki ConfigureServices ve Configure metodlarında bazı değişiklikler yapılması gerekiyor.

//Diğer isim alanları
using Swashbuckle.AspNetCore.Swagger;

namespace Fabrika
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
			//Diğer kodlar
			
			services.AddSwaggerGen(c =>
			{
				c.SwaggerDoc("v1", new Info { Title = "Fabrika API", Version = "v1" });
			});
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
			//Diğer kodlar
			
			app.UseSwagger();
			app.UseSwaggerUI(c=>
			{
				c.SwaggerEndpoint("/swagger/v1/swagger.json","Fabrika API v1.0");
			});
        }
    }
}

İki önemli ek söz konusu. İlk olarak ConfigureServices metodu içerisinde ilgili Swagger servisinin orta katmana eklenmesi sağlanıyor. Configure fonksiyonunda ise kullanıcı arayüzü için gerekli json içeriğinin adresi(Endpoint bilgisi) belirtilmekte ve Swagger çatısının kullanılacağı ifade edilmekte. Bu ekleri yaptıktan sonra aşağıdaki adrese talep gönderdim ve otomatik olarak üretilen bir JSON çıktısı ile karşılaştım.

http://localhost:5555/swagger/v1/swagger.json

Sonrasında ise takip ettiğim MSDN dokümanının söylediği gibi doğrudan swagger adresine gittim.

http://localhost:5555/swagger/

Sonuç inanılmaz güzeldi benim için(Otursam böyle bir tasarım yapamayacağım için olsa gerek) Kendimi çok fazla yormadan hazır bir swagger paketini kullanarak söz konusu API operasyonlarını görebileceğim, test edebileceğim bir içeriğe ulaştım.

Artık Fabrika API'sinin yardım sayfası en temel haliyle hazır diyebiliriz. Pek tabi bunu özelleştirmek de gerekiyor ki gayet güzel bir şekilde özelleştirebiliyoruz. Açıklamaları genişletebiliyor, XML Comment'leri kullanarak operasyonlar hakkında daha detaylı bilgiler verebiliyoruz vs... Şu adreste bu konu ile ilgili detaylı bilgiye ulaşabilirsiniz. 

Mesela AddSwaggerGen fonksiyonunu aşağıdaki gibi zengileştirebiliriz.

services.AddSwaggerGen(c =>
{
	c.SwaggerDoc("v1", new Info { 
		Title = "Fabrika API"
		, Version = "v1" 
		, Description ="Fabrika'da üretilen ürünler hakkında bilgiler"
		, Contact=new Contact{
			Name="Burak Selim Şenyurt"
			, Email="", Url="http://www.buraksenyurt.com"},
		License=new License{
			Name="Under GNU"
			, Url="http://www.buraksenyurt.com"}
		});
});

Son Sözler

Yazıyı bitirmekte olduğum şu anda içim biraz olsun huzurla doldu beynimdeki dopamin salınımı da arttı diyebilirim. En azından .Net Core 2.0'ı kullanarak, Visual Studio ailesine de el atmadan Notepad++ ile REST tabanlı basit bir Web API servisi yazabildim. Şimdi bunu evdeki Ubuntu üzerinde yapmaya çalışacağım.

Tabii gerçek hayat senaryolarında durum biraz daha farklı. Bir firmanın dışarıya açacağı servis bazlı API'leri düşünelim. Manuel olarak tek tek servis yazmak istenmeyecektir. Şirket içindeki hazır veri üretimi yapan birimlerin dinamik kodlar yardımıyla ayağa kalkacak servisler şeklinde sunulmasına çalışılacaktır. Kısacası bu tip Web API'leri bir Factory yardımıyla dinamik olarak nasıl üretebiliriz sorusu da gündeme geliyor. Örneğin şirketinizde n sayıda kütüphanenin belirli fonksiyonlarının Web API'ler ile açılacağını düşünün. Her bir kütüphane için Web API servisi yazmaya çalışmak yerine otomatik olarak bunları ayağa kaldıracak, yetkilendirmelere tabii tutacak bir mekanizma yazmak çok daha avantajlı olacaktır. Bu açılardan konuyu düşünmemizde ve öğrenmeye devam etmemizde yarar olduğu kanısındayım. Böylece geldim bir makalemin daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Yazmaya Üşenenler İçin

ProductsController.cs (2,38 kb)

FabrikaContext.cs (271,00 bytes)

Product.cs (161,00 bytes)

Program.cs (665,00 bytes)

Startup.cs (1,31 kb)


Asp.Net Core'da Bir WebSocket Macerası

$
0
0

Merhaba Arkadaşlar,

Meşhur ve özlenen telefon markası Nokia'nın o başarılı sloganını hatırlıyor musunuz?, "Nokia, connecting people" :) İşte bugünkü konumuz da o jeneriği aratmayacak türden. "WebSockets, connecting applications."(Burayı o adamın ses tonu ile zihninizde canlandırın derim) Evet berbat bir giriş oldu ama olsun. Gelelim konumuza.

Web Sockets aynı anda çift yönlü haberleşmeye olanak sağlayan bir protokol olarak karşımıza çıkmakta. HTTP'nin klasik Request/Response modelinden farklı çalışan bu model ile masaüstü uygulamalarının eş zamanlı olarak karşılıklı haberleşebilme yetenekleri web ortamına da taşınmış oluyor. Böylece akıllı telefonlardan, tabletlerden, masaüstü uygulamalarından, tarayıcılardan bir sunucu ile Web Socket haberleşmesi gerçeklenebiliyor. Bu iletişim de istemci ve sunucu birbirlerini beklemeden eş zamanlı olarak karşılılklı paket alışverişinde bulunmakta. Chat arabirimleri, Bot programları, borsa ürünleri, gerçek zamanlı oyunlar ve benzeri örneklerde Web Socket modeli kullanılabiliyor. Aslında TCP haberleşmesini web için çift yönlü iletişime dönüştüren bir model olduğunu da belirtebiliriz. En temelinde ise handshaking adı verilen bir kavrama dayanıyor. Sunucu, kendisine bağlı olan istemcileri ile senkronize kalıyor. Web Socket'lerin en önemli yanları ise gerçek anlamda eşzamanlılık ve optimize edilmiş bir mesaj trafiği sunması.

WebSockets protokolü Internet Engineering Task Force tarafından önerilmektedir. Standarda ait detaylı bilgiler için IETF'un ilgili sayfasına bakabilirsiniz.

Epey zamandır varlığından haberdar olduğumuz bu modelin Asp.Net tarafında daha da geliştirilmiş bir versiyonu var; SignalR. Ancak Asp.Net Core tarafına baktığımızda SignalR desteğinin henüz tam olarak gelmediğini görüyoruz(En azından araştırmayı yaptığım tarih itibariyle durum böyleydi. Gerçi yazıyı yayınladığım tarih itibariyle desteği gelmiş olsa da sorun değil. Ben Web Sockets kullanımını merak ettiğim için yazıda ısrar ettim)Şuradaki github adresinden Asp.Net Core tarafındaki SignalR gelişmeleri takip edilebilir.

Bu arada WebSockets ile geliştirme yapmak için temel HTML ve Javascript bilgisi yeterlidir. Hatta Tutorials Point'te güzel bir eğitim seti de bulunuyor. İsterseniz platform bağımsız olarak bu adresten de çalışabilirsiniz.

Neyse ki Asp.Net Core tarafında WebSockets desteği mevcut. Tabii SignalR bu işin nirvanası gibi duruyor ama ben Web Socket kullanımını merak etmekteyim. O zaman kolları sıvayalım ve yeni bir Asp.Net Core projesi oluşturarak işe koyulalım. Bakalım olayın özünü anlayabilecek miyim?

Projenin Oluşturulması

Örneği her zamanki gibi şirket bilgisayarında geliştirmekteyim. Dolayısıyla Windows 7 tabanlı bir işletim sisteminde .Net Core 2.0 ortamı olduğunu ifade edebilirim. Önceki yazıda kurdurttuğum ortam. Terminalden aşağıdaki komutu vererek HelloWebSockets isimli projeyi oluşturarak başlayalım.

dotnet new web -o HelloWebSockets

Sıradaki operasyon Microsoft.AspNetCore.WebSockets paketinin aşağıdaki terminal komutu ile projeye eklenmesi. Bu sayede System.Net.WebSockets isim alanı altında yer alan tipleri kullanabileceğiz.

dotnet add HelloWebSockets.csproj package Microsoft.AspNetCore.WebSockets

Kodlar

Artık gerekli kodları yazarak ilerleyebiliriz. İlk olarak Startup sınıfındaki Configure metoduyla uğraşacağız. Burada çalışma zamanına WebSockets kullanacağımızı bildireceğiz ve istemci ile sunucu arasındaki iletişimi açıp hareket eden mesajların kontrolünü sağlayacağız. Amacımız istemciden gelen mesajı yakalamak ve ona bir şeyler söylemek. Örneğin bugünkü şanslı numarasının ne olduğunu...

Startup.cs

using System;
using System.Threading;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using System.Net.WebSockets;

namespace HelloWebSockets
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseWebSockets();
            app.Use(async (ctx, nextMsg) =>
            {
                Console.WriteLine("Web Socket is listening");
                if (ctx.Request.Path == "/nokya")
                {
                    if (ctx.WebSockets.IsWebSocketRequest)
                    {
                        var wSocket = await ctx.WebSockets.AcceptWebSocketAsync();
                        await Talk(ctx, wSocket);
                    }
                    else
                    {
                        ctx.Response.StatusCode = 400;
                    }
                }
                else
                {
                    await nextMsg();
                }
            });

            app.UseFileServer();
        }

        private async Task Talk(HttpContext hContext, WebSocket wSocket)
        {
            var bag = new byte[1024];
            var result = await wSocket.ReceiveAsync(new ArraySegment<byte>(bag), CancellationToken.None);
            while (!result.CloseStatus.HasValue)
            {
                var incomingMessage = System.Text.Encoding.UTF8.GetString(bag, 0, result.Count);
                Console.WriteLine("\nClient says that '{0}'\n", incomingMessage);
                var rnd = new Random();
                var number = rnd.Next(1, 100);
                string message = string.Format("Your lucky Number is '{0}'. Don't remember that :)", number.ToString());
                byte[] outgoingMessage = System.Text.Encoding.UTF8.GetBytes(message);
                await wSocket.SendAsync(new ArraySegment<byte>(outgoingMessage, 0, outgoingMessage.Length), result.MessageType, result.EndOfMessage, CancellationToken.None);
                result = await wSocket.ReceiveAsync(new ArraySegment<byte>(bag), CancellationToken.None);
            }
            await wSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
        }
    }
}

Kodlar biraz korkutucu görünebilir. Bir sürü async, await görmekteyiz. Asenkron bir şeyler oluyor. Kısaca üzerinden geçelim. İlk odaklanacağımız nokta Configure metodu. Web Sockets kullanacağımızı UseWebSockets fonksiyonu ile ortama bildiriyoruz. Takip eden satırda ise istemcileri dinlemeye başlıyoruz. Burası asenkron çalışan bir kod bloğu. İçinde /nokya adresine gelen bir talep olup olmadığına bakılıyor. Eğer bu adrese gelen bir talep varsa ve gelen bu talep bir Web Sockets isteğiyse(ki bunun ws:// protokolü ile başlayan bir talep olması gerekiyor) bir socket nesnesi yakalıyor ve Talk isimli fonksiyonu çağırıyoruz(Yine asenkron olarak tabii) Eğer gelen istek /nokya adresine yapılmamışsa bir sonraki mesajı bekleyecek şekilde dinleme işlemine devam ediliyor. Özetle o anki context'i ve mesajı yakalayıp bunun bir Web Sockets iletişimi olup olmadığının anlaşılması ve buna göre Talk operasyonunun çağırılması söz konusu.

Talk operasyonu içerisinde istemciden gelen mesajın terminale basılması ve rastgele üretilen bir sayı değerinin geri gönderilmesi için gerekli kodlar yer alıyor. İstemcinin gönderdiği mesajın yakalanması için ReceiveAsync isimli metoddan yararlanılmakta. Metodun ilk parametresi gelen içeriği bir Byte dizisine alıyor. Eğer aradaki iletişim açıksa(CloseStatus.HasValue ile bu kontrol edilmekte) bu byte içeriğini string'e dönüştürmek yeterli. Bağlı olan istemciye soket üzerinden mesaj göndermek içinse SendAsync metodundan yararlanılmakta. Trafik byte array'ler ile çalışıyor bu nedenle string operasyonları için UTF8 tabanlı Encoding mekanizmalarından yararlanılmakta. 

Program.cs

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

namespace HelloWebSockets
{
    public class Program
    {
        public static void Main(string[] args)
        {
            BuildWebHost(args).Run();
        }

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

Program.cs içeriğinde çok fazla müdahalemiz yok. Sadece kullanılacak port bilgisini ayarlıyoruz. Buna göre http://localhost:5556 adresinden yayın yapacağız ve istemciler ws://localhost:5556/nokya adresini kullanarak Web Sockets temelli iletişimi gerçekleştirebilecekler. Örnekte istemci olarak index.html sayfasını ele alacağız. wwwroot klasöründe konuşlandıracağımız sayfanın içeriğini aşağıdaki gibi yazabiliriz.

index.html

<html><head><title>Asp.Net Core ile Web Sockets Kullanımı</title></head><body><button id="btnConnect" type="submit">Connect</button><br/>
Message : <input id="lblMessage" style="width:300;" /><br/><button id="btnSendMessage" type="submit">Send Message</button><br/><button id="btnDisconnect" type="submit">Disconnect</button><br/><script>
    var btnConnect = document.getElementById("btnConnect");
    var btnSendMessage=document.getElementById("btnSendMessage");
	var lblMessage=document.getElementById("lblMessage");
	var btnDisconnect=document.getElementById("btnDisconnect");
	var socket;
	btnConnect.onclick = function() {
            socket = new WebSocket("ws://localhost:5556/nokya");
            socket.onopen = function (e) {				
                console.log("Connected",e);
            };
            socket.onclose = function (e) {
                console.log("Disconnected",e);
            };
            socket.onerror = function(e){
				console.error(e.data);
			};
            socket.onmessage = function (e) {
                console.log(e.data);
            };
		}
	btnSendMessage.onclick = function () {
            if (!socket || socket.readyState != WebSocket.OPEN) {
                console.error("Houston we have a problem! Socket not connected.");
            }
            var data = lblMessage.value;
            socket.send(data);
            console.log(data);
        }
	btnDisconnect.onclick = function () {
			if (!socket || socket.readyState != WebSocket.OPEN) {
				console.error("Houston we have a problem! Socket not connected.");
			}
			socket.close(1000, "Closing from Apollo 13");			
        }</script></body>

Temel olarak bağlantıyı açmayı, text kutusuna yazılan içeriği karşı tarafa göndermeyi, aradaki iletişimi debug penceresinden izlemeyi ve son olarak da iletişimi başarılı şekilde sonlandırmayı hedefliyoruz. İçerikteki en değerli kodlarımız btnConnect butonuna basıldığında çalışıyor. Dikkat edileceği üzere bir WebSocket nesnesi üretilmekte. Adres olarak da ws://localhost:5556/nokya adresi kullanılıyor. Adresin ws ile başladığına dikkat edelim. Oluşturulan WebSockets nesnesi için bazı olaylar tanımlanıyor. Bağlantı açıldığında, kapatıldığında, sunucudan mesaj alındığında veya bir hata oluştuğunda. Sunucuya mesaj göndermek için WebSocket nesnesinin send fonksiyonundan yararlanılmakta.

Sonuçlar

Öncelikli olarak 

dotnet run

komutuyla web sunucusunun çalıştırılması gerekiyor. Sonrasında herhangibir tarayıcıdan(Örnekte Google Chrome kullanılmıştır) http://localhost:5556/ adresine gidilmesi yeterli. Önce bağlanıp ardından mesaj gönderebiliriz. Sonrasında da iletişimi kapatmamız yeterli. İşte benim elde ettiğim sonuçlar.

Tarayıcı tarafı;

Dikkat edileceği üzere aradaki haberleşme console ekranına düşmektedir(Chrome'da debug penceresini açmak için F12 tuşunu kullanabilirsiniz) Ayrıca "Don't remember" nedir yahu...

Sunucu tarafı

Sunucu tarafında da istemciden gelen mesajların başarılı bir şekilde yakalandığı görülmektedir.

Dilerseniz n sayıda istemciyi bağlayabilir ve herbirinin ayrı bir konuşma içerisinde değerlendirildiğini de test edebilirsiniz. Aşağıdaki ekran görüntüsünde olduğu gibi.

Asp.Net Core ile Web Sockets haberleşmesi yapmak oldukça kolay. Bu örneği tabii diğer platformalarda da denemek lazım. İstemciyi çeşitlendirebiliriz. Bir mobil uygulama, desktop uygulaması veya farklı bir servis dahi olabilir. Hatta örnek daha da zenginleştirilebilir ve biraz oyunlaştırılabilir. Bir sayı tahmin oyununu bu şekilde yapmaya çalıştığınızı düşünün. Hatta istemcinin ekranına soru seti gönderip, cevaplarına göre hikayeleştirdiğiniz bir senaryoyu işletebilirsiniz. Şu sıralar tabletten oynadığım My Cafe isimli oyunda restorana gelen müşterilerim ile aramda böyle hikayeleştirilmiş soru-cevap olayları söz konusu. Acaba Web Sockets mi kullanılmış merak etmekteyim. Böylece geldik bir makalemizin daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim. 

WCF - Özelleştirilmiş UsernamePasswordValidator Kullanımı

$
0
0

Merhaba Arkadaşlar,

Yeni ekibimdeki çalışmalar doğrultusunda bir süredir servis tabanlı mimarimizde WCF üzerine oturan hafif bir çatı oluşturmaya çalışmaktayız. Önemli ölçüde ilerleme kaydettik. Tabii WCF'in en temel fonksiyonelliklerini kullanırken ne kadar geniş bir alan olduğunun da farkına varıyoruz. Bizi zorlayan pek çok nokta var. Bunlardan birisi de güvenlik. Neredeyse sayısız kombinasyon seçeneği ile WCF tarafındaki güvenlik yetenekleri çok geniş(Bazen ne istediğimizi bile bilemez duruma geldiğimizi itiraf etmek isterim) Hal böyle olunca benim de eski bilgilerimi tazeleyip bazı şeyleri yeniden araştırmam ve öğrenmeye çalışmam gerekti. En çok takıldığım noktalardan birisi de geliştirme ortamında sertifikalar üretip bunları WCF tarafında kullanmak. Not aldığım konular üzerinden adım adım giderken belki benim yolumdan geçen veya geçecek olan arkadaşlar da vardır diye vakalardan birisini bloğumda kaleme almak istedim.

Senaryomuz şu; istemcilerin özel bir doğrulama mekanizması ile ele alınmasını sağlamak istiyoruz. Ancak bunu mesaj seviyesinde güvenli olan bir iletişim hattı üzerinde gerçekleştirmeliyiz. Binding konusunda serbestiz. Bir başka deyişle WsHttpBinding'i Message Based güvenlik modunda kullanıp özel bir UserNamePasswordValidator tipi ele alacağız. Mesaj tabanlı güvenlik söz konusu olduğu için sunucu ve istemcinin birbirlerine olan güvenini sertifikalarıyla sağlamamız gerekiyor. Biliyorum terimlerle kavramlar birbirine girdi ve kafalar karıştı. O zaman gelin adım adım ilerlemeye çalışalım.

Sertifikaların Oluşturulması

Aynı ortamda geliştirme yapmaktayız. Sunucu ve istemcinin birbirlerini doğrulaması noktasında iki adet sertifikaya ihtiyacımız olacak. Bu sertifikaları makecert aracını kullanarak üretebiliriz.

İlk olarak sunucu sertifikasını oluşturalım.

C:\C\Certificates>makecert -sr CurrentUser -ss My -a sha1 -n "CN=AzonServer" -sky exchange -pe

Benzer şekilde istemci sertifikasını...

C:\C\Certificates>makecert -sr CurrentUser -ss My -a sha1 -n "CN=AzonServer" -sky exchange -pe

Komutlarda kullanılan anahtarların belli anlamları var. Örneğin -sr ile kayıt lokasyonunu(Registry Location), -ss ile sertifika deposunu(Certificate Store), -a ile hangi kriptografi algoritmasını kullanacağımızı(MD5, SHA1 gibi), -n ile üreteceğimiz sertifikanın genel adını(Common Name), -sky ile anahtar tipini(Exchange, Signature gibi), -pe ile ilgili anahtarın ihraç edilip edilemeyeceğini(Exportable) belirtmekteyiz. Oluşturulan sertifikalar o anki kullanıcı için Personal - Certificates deposuna eklenecektir. Bunu Microsoft Management Console(MMC) aracı ile görebiliriz. Komut satırından MMC aracını açıp File - Add Remove/Snap-in ile Certificates sekmesini ekleyelim. Bu durumda AzonServer ve AzonClient sertifikalarının aşağıdaki ekran görüntüsünde olduğu gibi ilgili depoya dahil edildiklerini görebiliriz.

Sertifika üretimleri geliştireceğimiz örnekte ilerlememiz için yeterli değil. İlgili sertifikaları WCF çalışma zamanında hem sunucu hem de istemci tarafında kullanabilmek için Trusted People sekmesi altına da kopyalamamız gerekiyor. Dolayısıyla bir gerçek hayat senaryosunda bu sertifikaların birbirleriyle konuşacak olan uygulama sunucularında da yüklü olması lazım.

Sunucu Tarafının Geliştirilmesi

Sunucu tarafında basit bir servisimiz bulunacak. Bu servisi WsHttpBinding destekli olacak şekilde konuşlandıracağız. Kullanacağı güvenlik ayarlarını bu örnek özelinde programatik olarak düzenleyeceğiz. Tabii bir de özel kullanıcı doğrulama sınıfımızı ilave edeceğiz. Host uygulamasını Console tabanlı bir proje olarak geliştirebiliriz. System.ServiceModel ve System.IdentityModel kütüphanelerinin projeye referans edilmesi önemli. Önce servis sözleşmesini ve ilgili servis tipini yazalım.

Servis Sözleşmesi:

using System.ServiceModel;

namespace AzonHostApp
{
    [ServiceContract]
    public interface IMathService
    {
        [OperationContract]
        double Sum(double x, double y);
    }
}

Servis tipi:

namespace AzonHostApp
{
    public class MathService
        : IMathService
    {
        public double Sum(double x, double y)
        {
            return x + y;
        }
    }
}

Kobay sınıfımız toplama işlemi içeren bir servis sunmakta. Şimdi özel kullanıcı doğrulama(Custom Authentication) işlemini üstlenecek sınıfı yazalım.

using System.IdentityModel.Selectors;
using System.IdentityModel.Tokens;

namespace AzonHostApp
{
    public class AzonUsernamePasswordValidator
        :UserNamePasswordValidator
    {
        public override void Validate(string userName, string password)
        {
            if(userName!="barbarian"||password!="P@ssw0rd")
                throw new SecurityTokenException("");
        }
    }
}

AzonUsernamePasswordValidator sınıfı UserNamePasswordValidator tipinden türemekte. Üst tipten gelen Validate fonksiyonunun ezildiğine(override) dikkat edelim. Örneği oldukça basit bir şekilde ele almak istediğimizden tek yaptığımız belli bir kullanıcı ve şifresini kontrol etmekten ibaret. Önemli olan ise geçersiz oldukları takdirde bir SecurityTokenException fırlatıyor olmamız. Gerçek hayat senaryosunda buradaki kontrol operasyonunun bir Identity Server üzerinden gerçekleştirilmesi de düşünülebilir.

Host Uygulamanın Geliştirilmesi

Gelelim host tarafına. Burada standart olarak servis çalışma zamanını ayağa kaldıracak işlemler yapacağız. Normalde konfigurasyon bazlı olarak da ilerleyebiliriz. Ne var ki projelerimizde standart .config seçenekleri dışında kod yoluyla bir takım yetenekleri ortama dahil ediyoruz. Aslında tasarlanacak IoC yapısındaki konfigurasyon seçenekleri ile WCF çalışma ortamını genişletmeyi planladığımızı itiraf edebilirim. Lafı fazla uzatmadan Main metodundaki kodları aşağıdaki gibi düzenleyerek devam edelim.

using System;
using System.Security.Cryptography.X509Certificates;
using System.ServiceModel;
using System.ServiceModel.Description;
using System.ServiceModel.Security;

namespace AzonHostApp
{
    class Program
    {
        static void Main(string[] args)
        {
            ServiceHost host = new ServiceHost(typeof(MathService), new Uri[] { new Uri("http://localhost:6002") });
            host.Description.Behaviors.Add(new ServiceMetadataBehavior() { HttpGetEnabled = true, HttpsGetEnabled = true });
            host.Description.Behaviors.Find<ServiceDebugBehavior>().IncludeExceptionDetailInFaults = true;
            host.Credentials.ClientCertificate.Authentication.CertificateValidationMode = X509CertificateValidationMode.PeerOrChainTrust;
            host.Credentials.ServiceCertificate.SetCertificate(StoreLocation.CurrentUser, StoreName.My, X509FindType.FindBySubjectName, "AzonServer");

            WSHttpBinding binding = new WSHttpBinding();
            binding.Security.Mode = SecurityMode.Message;
            binding.Security.Message.ClientCredentialType = MessageCredentialType.UserName;
            host.AddServiceEndpoint(typeof(IMathService), binding, "soap12");
            host.Credentials.UserNameAuthentication.UserNamePasswordValidationMode = UserNamePasswordValidationMode.Custom;
            host.Credentials.UserNameAuthentication.CustomUserNamePasswordValidator = new AzonUsernamePasswordValidator();

            host.Open();
            if (host.State == CommunicationState.Opened)
            {
                Console.WriteLine("Host dinlemede. Kapatmak için bir tuşa basın");
                Console.ReadLine();

                host.Close();
                Console.WriteLine("Host kapatıldı");
                Console.ReadLine();
            }
        }
    }
}

Şimdi neler yaptığımıza bir bakalım. ServiceHost nesnesini üretirken hangi tipi kullanacağımızı ve adres bilgisini veriyoruz. Buna göre servisimiz http://localhost:6002 adresinden yayınlanacak. WSDL ve Exception detayı paylaşımı için çalışma zamanına varsayılan olarak eklenen ServiceMetadataBehavior ve ServiceDebugBehavior niteliklerini yakalayıp gerekli özelliklerini true olarak belirliyoruz. Sonrasında ise istemci ve sunucu arasındaki sertifika doğrulama işlemlerinin hangi modda yapılacağını belirtmekteyiz. Örnekte PeerOrChainTrust kullandık. Aslında farklı Trust modları bulunuyor(Detaylar için şu adrese bakabilirsiniz) Devam eden kodda Comman Name değerini AzonServer olarak verdiğimiz sertifikanın bildirimi gerçekleştiriliyor. Sertifikanın CurrentUser deposunda SubjectName'e göre aranacağı belirtilmekte.

Örnekte Ws standartlarını destekleyen bir binding tipi kullanılmakta. Bu tipin güvenlik modunu mesaj tabanlı olacak şekilde belirliyoruz. İstemcinin de kullanıcı adı ve şifre doğrulamasına tabi tutulacağını ClientCredentialType özelliğine atadığımız değerle işaret etmekteyiz. Bu ayarlamalardan sonra ilgili ServiceEndpoint tipinin eklenmesi söz konusu. Host tarafında kullanıcı doğrulama işlemi için AzonUsernamePasswordValidator isimli bir sınıf yazmıştık. Bu tipin kullanılacağını belirtmemiz lazım. Bu nedenle öncelikle UserNamePasswordValidationMode değerini Custom'a çekip CustomUserNamePasswordValidator özelliğine de kendi nesne örneğimizi ekliyoruz. Tabii burada işin sırrı bu atamanın gerçekleşmesi için AzonUsernamePasswordValidator tipinin System.IdentityModel.Selectors isim alanındaki UserNamePasswordValidator tipinden türemiş olması(İşte size bir çalışma zamanının basit genişletilebilirlik tasarımı)

Son olarak Open ve Close metodları kullanılarak gerekli açma ve kapatma işlemlerinin tatbik edildiğini belirtelim. Console uygulamasını bu haliyle çalıştırdığımızda aşağıdaki sonuçları görmemiz gerekiyor.

İstemci Tarafının Geliştirilmesi

Artık istemci tarafını yazmaya başlayabiliriz. Onu da basitlik olması açısından bir Console uygulaması olarak geliştirelim. Host uygulaması açıkken aşağıdaki ekran görüntüsünde olduğu gibi servis referansını istemci tarafına ekleyebiliriz.

İstemci tarafına ait kodları da aşağıdaki gibi yazabiliriz.

using AzonClientApp.Azon;
using System;

namespace AzonClientApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Test için bir tuşa basın");
            Console.ReadLine();

            MathServiceClient client = new MathServiceClient("WSHttpBinding_IMathService");
                      
            client.ClientCredentials.UserName.UserName = "barbarian";
            client.ClientCredentials.UserName.Password = "P@ssw0rd";
            double result = client.Sum(4.12, 3.41);

            Console.WriteLine(result);
        }
    }
}

Dikkat edilmesi gereken nokta MathServiceClient nesne örneği üzerinden ClientCredentials bilgisinin doldurulması. UserName üzerinden kullanıcı adı ve şifre bilgilerini belirttikten sonra Sum operasyonunu çağırmaktayız. Şimdi test sürüşüne çıkabiliriz. Önce sunucu sonra da istemci uygulamaları çalıştıralım. Ne yazık ki aşağıdakine benzer bir hata ile karşılaşma olasılığımız yüksek.

Sertifikanın doğrulanması sırasında bir hata oluştuğu ortada. Servis referansının eklenmesi sonrası oluşan web.config içeriğine biraz müdahalede bulunmamız gerekiyor. İçeriği aşağıdaki hale getirerek devam edelim.

<?xml version="1.0" encoding="utf-8" ?><configuration><startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" /></startup><system.serviceModel><bindings><wsHttpBinding><binding name="WSHttpBinding_IMathService"><security><message clientCredentialType="UserName" /></security></binding></wsHttpBinding></bindings><behaviors><endpointBehaviors><behavior name="EndpointBehaviorForCertificate"><clientCredentials><clientCertificate findValue="AzonClient" x509FindType="FindBySubjectName"
                storeLocation="CurrentUser" storeName="My" /><serviceCertificate><authentication certificateValidationMode="PeerOrChainTrust"/></serviceCertificate></clientCredentials></behavior></endpointBehaviors></behaviors><client><endpoint address="http://localhost:6002/soap12" binding="wsHttpBinding"
                bindingConfiguration="WSHttpBinding_IMathService" contract="Azon.IMathService"
                name="WSHttpBinding_IMathService" behaviorConfiguration="EndpointBehaviorForCertificate"><identity><dns value="AzonServer"/></identity></endpoint></client></system.serviceModel></configuration>

Aslında bir endpointBehavior ekledik. EndpointBehaviorForCertificate kısmında istemci tarafına ait sertifika bildirimini yapmaktayız. AzonClient isimli sertifikanın kullanılacağını ifade ediyoruz. Diğer yandan servis sertifikasının doğrulama modelini de PeerOrChainTrust olarak verdiğimize dikkat edelim. Gerçi sunucu tarafında da bu şekilde belirttiğimizden istemci tarafında sadece PeerTrust(Bir Chain Store olmadığından ChainStore değil) da kullanabiliriz. İkinci değişiklik ise endpoint'e ait identity elementinde yer alan dns değeri. Burada servise ait sertifikanın Common Name bilgisinin verildiği görülmekte. Uygulamaları tekrar çalıştırdığımızda aşağıdaki gibi başarılı bir çağırım gerçekleştirdiğimizi görebiliriz.

Elbette hatalı kullanıcı bilgisi ile ilerlenirse bir istisna alınacağı aşikardır.

Sonuç

Bu örnekte istemci ve servis arasında WS standartlarında mesaj tabanlı güvenlikle sağlanan bir iletişim gerçekleştirildiğini gördük. Ayrıca istemciyi kendi doğrulama modelimize dahil ettik. Kritik nokta bu örnekte yer alan sunucu ve istemcinin farklı makinelerde birer uygulama olması hali. Böyle bir vakada AzonServer ve AzonClient isimli sertifikaların her iki makinenin Trusted People kısmında yüklü olması gerekecektir. İstemci ve sunucuyu ayrı birer uygulama sunucusu olarak da düşünebiliriz. Örneği farklı güvenlik modları ile denemenizi öneririm. Örneğin Transport seviyesinde güvenlik moduna geçmeyi deneyebilirsiniz. Bu durumda https şemasını destekleyecek bir sunucuya da sahip olabilirsiniz. İlk başta da belirttiğim üzere WCF tarafındaki güvenlik bazlı senaryolar ve kullanılabilecek kombinasyon oldukça fazla. Benim bu örnekte yaptığım gibi denemelerden yararlanarak kendiniz keşfetmeye çalışırsanız öğrendiklerinizin daha kalıcı olacağını görebilirsiniz. Böylece geldik bir makalemizin daha sonuna. Tekrarda görüşünceye dek hepinize mutlu günler dilerim.

Tek Fotoluk İpucu 160 - Bir Sertifikanın Base64 Encoded Değerini Bulmak

$
0
0

Merhaba Arkadaşlar,

Önceki yazımızda WsHttpBinding kullandığımız sertifika tabanlı bir WCF senaryo çalışmamız vardı. Aynı örneği göz önüne alarak BasicHttpBinding kullanabileceğimizi de belirtelim. Nitekim bu bağlayıcı tipi ile de Message tabanlı güvenliği sertifika bazlı gerçekleştirebiliriz. Bunun en gerekli sebeplerinden birisi de servis tüketicilerinin eski nesil uygulamalar olabilmesi sebebiyle sadece SOAP 1.1 haberleşme kurmasıdır. Olmaz demeyin oluyor. Geliştirmekte olduğumuz projede buna benzer bir ihtiyaçla karşılaştık. Bazı servis tüketicilerimiz sadece SOAP 1.1 paketi gönderebilir durumdalar. Tabii öncelikle bizim .Net ortamında bu senaryoyu test edebilmemiz gerekmekteydi. Bağlayıcı tipini belirledik, Message güvenliğini ayarladık, sertifika tanımlamalarını yükledik ve servisi ayağa kaldırıp istemciye proxy tipini indirttik. İstemci web.config dosyasında gerekli ayarlamaları yaptık. Ne varki istemci tarafındaki endpoint bildiriminde yaptığımız aşağıdaki örnek tanımlama işe BasicHttpBinding tipi özelinde işe yaramadı ve çalışma zamanında "The request message must be protected" şeklinde hata aldık.

<identity><dns value="AzonServer"/></identity>

Bunun üzerine istemci tarafında belirtilmesi gereken sertifikanın base64 formatında encode edilmiş halini denemeye karar verdik. Yani aşağıdaki şekilde.

<identity><certificate encodedValue=""/></identity>

Tabii bir sertifikanın base64 bazlı halini nasıl elde edeceğimizi bilmiyorduk. Öğrendik. Nasıl mı oluyormuş? Aynen aşağıdaki ekran görüntüsündeki gibi.

Koddaki AzonServer isimli sertifika CurrentUser->My deposu altında yer alıyor. Bu depoya ulaşmak için bir X509Store nesnesi örnekliyoruz. Sonrasında Certificates koleksiyonuna gidiyor ve SubjectName değerine göre(ki bu Comman Name'e denk gelecektir) arama yapıyoruz. Tabii ilgili sertifikanın olduğunu varsayıyoruz. Yoksa array için çalışma zamanı hatası alacağımız aşikar. Sertifikayı elde ettikten sonra bunu ihraç ediyor ve ToBase64String fonksiyonundan yararlanarak base64 encode edilmiş halini elde ediyoruz. Elde ettiğimiz içeriği de Clipboard'a(System.Windows.Forms kütüphanesini referans etmeli ve STAThread niteliğini kullanmalıyız) kopyalamayı ihmal etmiyoruz ki sonrasında tek yapacağımz şey Ctrl+V olsun. Bu hızlı ve ani çözümü uygulamak için Powershell'den de yararlanabilirsiniz bunu da belirteyim. Araştırın derim.

Bir başka ipucunda görüşmek üzere hepinize mutlu günler dilerim.

Ubuntu'da İlk .Net Core Adımlarım

$
0
0

Merhaba Arkadaşlar,

Üniversite yıllarımda internet yeni yeni yaygınlaşmaya başlayan bir ortamdı. 14400 kpbs hızındaki modemimi daha 3ncü sınıfta alabilmiştim. Bu sebepten okulun ilk yıllarında bilgisayar teknolojileri ile ilgili bilgileri öğrenebileceğim en güzel mecra aylık dergilerdi. Bazıları zaman içerisinde kapandı bazılarıysa internet üzerinden yayınlanmaya başladı. Ben ağırlıklı olarak PcWorld, PcNet, PcMagazine gibi dergileri okuduğumu hatta ay ay biriktirdiğimi hatırlıyorum.

Byte severek takip ettiğim dergilerden birisiydi. Özellikle de Jerry Pournelle'in köşesi. Her ay farklı şeylerle uğraşır, yaptığı denemelerin sonuçları paylaşır, bilim kurgu üzerine konuşur, kitap önerilerinde bulunurdu. Yeni yazılımları dener ve deneyimlerini aktarırdı. The New York Times ilgili yazısında onu bilim kurgu yazarı ve bilgisayar rehberi olarak tanımlamıştır. Araştırmayı ve bu araştırmaları sırasında öğrendiklerini paylaşan değerli bir bilim insanıydı benim için.

Ne yazık ki 8 Eylül 2017 tarihinde aramızdan ayrılmış. Benim de bir şeyleri araştırıp bu araştırmalarla ilgili notlar almama vesile olan ve kendime göre bir gelişim metodolojisini alışkanlık haline getirmemi sağlayan insanlardan birisiydi. 

Bu düşünceler eşlinde hafta sonu işlerime başladım. Evdeki yaklaşık 6 yıllık Dell marka dizüstü bilgisayarımda uzun süredir iki işletim sistemi kullanıyordum. Ubuntu ve Windows 7. Bir süredir de Ubuntu üzerinde Docker kullanarak .Net Core 2.0 uygulamaları geliştirmeyi planlıyordum. Ancak bir türlü Community Edition sürümünü yükleyemedim. Sebep Linux'ün 64bit olmamasıydı. Windows 7 üzerinde Docker kullanabilirdim ama amacım Cross Platform deneyimini .Net Core için Linux üzerinde yaşamaktı. Öyleyse ilk etapta işi Docker olmadan halledeyim dedim. Yine de 64bit bir Ubuntu bana gerekliydi. Sonunda kararımı verdim ve dizüstünü sıfırlayarak üzerine sadece Ubuntu 64bit işletim sistemini kurdum(Şu adresteki sürümü kullandım)

Docker deneyimlerine geçmeden önce .Net Core 2.0 ile Ubuntu'da bir Hello World demeye karar verdim. Amacım bir Linux platformu üzerine .Net Core 2.0 SDK'yi yüklemek ve C# ile yazılmış bir Console uygulamasını kullanarak yeni bir ufka merhaba demekti. Benim için önemli bir başlangıç noktası. Nitekim önümüzdeki sene içerisinde işten arta kalan zamanlarda Linux üzerinde .Net Core 2.0 ile bir şeyler yapmaya gayret edeceğim. 

.Net Core'un Kurulumu

Terminal penceresini ve Microsoft'un konu ile ilgili dokümantasyonunu açtıktan sonra kurulum adımlarını takip etmeye başladım. Okuduğum kadarıyla öncelikli olarak Microsoft Product Feed listesine kayıt olmak gerekiyor. Bunu iki adımda gerçekleştiriyoruz. Önce güvenli bir anahtar sonrasında da kayıt. Sadece bir kere yapmamız yeterli. Kayıt işlemini gerçekleştirirken kullandığımız Ubuntu sürümünü de dikkate almalıyız. Ben Ubuntu 16.04 Linux Mint versiyonunu yüklediğim için buna uygun bir bildirim yaptım(Xenial ifadesinin kullanılmas sebebi) Bu arada curl komutunun genellikle internetten içerik indirmek için kullanıldığını öğrendim(Linux tarafında çok acemiyim biliyorsunuz ki)

curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg

sudo mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg

sudo sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/microsoft-ubuntu-xenial-prod xenial main" > /etc/apt/sources.list.d/dotnetdev.list'

Sorunsuz geçen bu sürecin ardından sistemi güncelleyip .Net Core 2.0 sürümünü indirmeye başladım. Tabii alışılageldiği üzere önce bir güncelleme yapmakta ve eksik paketlerin yüklenmesini sağlamakta yarar var. 

sudo apt-get update

sudo apt-get install dotnet-sdk-2.0.0

Eğer bir probem yoksa terminalden dotnet komutunu kullanarak versiyon numarasını ve güncel sürüm bilgilerini aşağıdaki gibi görebilmemiz gerekiyor.

dotnet --version

dotnet --info

Console Uygulamasının Oluşturulması

Bildiğiniz gibi dotnet new komutu ile önceden tanımlı şablonlardan yararlanarak projelerimizi oluşturabiliyoruz(Neler oluşturabileceğimizi görmek için dotnet new --help ifadesinden yararlanabilirsiniz) Benim ilk hedefim terminal pencersinde basit bir şeyler yazdırıp Hello World diyebilmek. Bu sebepten oluşturduğum Development klasörü üzerinde aşağıdaki ifadeyi kullanarak HelloWorld isimli bir Console projesi oluşturdum.

dotnet new console -o HelloWorld

Oluşan Program.cs içeriğini biraz değiştirdim ve Algebra.cs isimli bir sınıfı da klasöre dahil ettim.

Algebra.cs içeriği;

using System;

namespace Utility
{
    public class Algebra
    {
        public double Sum(double x,double y)
        {
            return x+y;
        }
    }
}

Program.cs içeriği;

using System;
using Utility;

namespace HelloWorld
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Merhaba");
            double x=1.23;
            double y=Math.PI;
            Algebra einstein=new Algebra();
            Console.WriteLine("{0}+{1}={2}",x,y,einstein.Sum(x,y));
        }
    }
}

Tahmin edeceğiniz üzere iki double sayının toplamını hesap eden Sum fonksiyonunu içeren Algebra isimli sınıfın basit bir kullanımı söz konusu. Projeyi derleyip çalıştırmak içinse build ve run parametrelerinden yararlanıyoruz.

dotnet build

dotnet run

Belki de pek çoğunuz için etkileyici bir sonuç değil ama benim oldukça hoşuma gitti. Vakti zamanında sadece Windows platformunda .Net ile kod yazabildiğimi düşününce, kurduğum Linux makinede bu ortamı kullanarak C# ile kod geliştirebilmek harika bir deneyim. 

Visual Studio Code Kurulumu

Bu arada kodları nasıl düzenlediğimi merak etmiş de olabilirsiniz. Tahmin edeceğiniz gibi Visual Studio Code'u kullandım. Bu uygulamayı da terminalden aşağıdaki komutları kullanarak yükleyebilirsiniz. Yükleme işleminden sonra HelloWorld uygulamasını açtığımızda C# için gerekli Extension'da öneri olarak sunulacaktır.

sudo sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/vscode stable main" > /etc/apt/sources.list.d/vscode.list'

sudo apt-get update

sudo apt-get install code

Eğer komut satırından kurulum zor geliyorsa basit kurulum için şu adresten de yararlanabilirsiniz.

Şimdi Ne Olacak?

Artık emektar dizüstü bilgisayarımda Ubuntu 64bit sürümü yüklü. Tertemiz bir ortamım var. Bu ortamda şu an için .Net Core 2.0 ve Visual Studio Code bulunuyor. Ama yapmam gereken başka işler var. GitHub ile entegre olmalıyım. Diğer yandan Docker kurulumunu gerçekleştirip bir .Net Core 2.0 imajı üzerinde çalışmalar yapmalıyım. Ayrıca GoLang, Ruby ve Python için de ortamı hazırlamalıyım. Bu arada Linux terminal için biraz komut çalışsam hiç de fena olmaz. Python tarafında sudo, apt-get gibi ifadelere aşina olsam da curl gibi yeni karşılaştığım komutların farkında olmalıyım. 

Görüldüğü gibi evinizdeki bilgisayarımıza Linux kurup üzerinde .Net Core 2.0 ile Hello World dememiz oldukça basit. Daha kolayı sanıyorum ki Docker Container kullanımı. Böylece geldik bir makalemizin daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Docker Üzerinde .Net Core Uygulaması Çalıştırmak

$
0
0

Merhaba Arkadaşlar,

Biliyorum epeyce geriden geliyorum yeni nesil konularda ama işler güçler derken ancak zaman bulabiliyorum. Önceki yazılarımdan da hatırlayacağınız üzere evdeki emektar dizüstü bilgisayarıma Ubuntu'nun 64bitlik sürümünü yüklemiştim(Makineye West-World adını verdim)Üzerinde ilk .Net Core denemelerimi de gerçekleştirdim. Ancak merak ettiğim konulardan birisi de Docker üzerinde bir .Net Core uygulamasının nasıl çalıştırılabileceğiydi. Bu iş sandığımdan daha zor olacaktı. Yarım yamalak bilgimle Docker'ın ne olduğunu az çok biliyordum ama tam anlamıyla da hakim değildim. En azından biraz daha fikir sahibi olmalı, kurulumunu gerçekleştirmeli ve sonrasında örnek bir .Net Core uygulamasını Dockerize ederek taze bir imaj(image) üzerinde ayağa kaldırabilmeliydim. 

Internet üzerinde Docker ile ilgili pek çok bilgi ve kaynağa ulaştım. Ama özellikle Asiye Yiğit'in Linkedin üzerinden paylaştığı yazılar önemli bilgiler edinmemi sağladılar. Bunun haricinde DevOps tarafında oldukça yetenekli olan arkadaşım(ki hemen solumda oturur) Alpay Bilgiç, beni aydınlatan bilgiler verdi. Ne sorsam cevapladı. Çıkarttığım notlardan yararlanarak konuyu kavramak için şekilleri tekrardan ele aldım. Öncelikle ilgili notları bu blog yazısı aracılığıyla temize çekeceğim ki yarın öbür gün nasıl oluyordu bu iş dediğimde dönüp bakabileyim. Sonrasında Ubuntu üzerine Docker kuracağım. Ardından .Net Core 2.0 için basit bir Console uygulaması yazacağım. Son adımda ise bu uygulamayı Docker üzerinde ayağa kaldıracağım. Haydi gelin başlayalım.

Docker'dan Anladığım

Aslında her şey farklı platformlarda çalışabilecek uygulamaların ölçek büyüdükçe daha çok makineye ve kuruluma ihtiyaç duyması sonrasında başlamış gibi duruyor. Yeni makine demek, yeni kurulumlar, yeni lisans ücretleri, yeni yönetim sorumlulukları, yeni dağıtım süreçleri, yeni elemanlar demek. Durum böyle olunca maliyetlerin artması da kaçınılmaz hale gelmiş. Benim üniversite yıllarında da tanık olduğum o eski yaklaşım kabaca aşağıdaki şekilde görüldüğü gibiydi.

İlk dünya yukarıdaki gibiydi. Sonrasında ise Hyper-V(Fiziki bir makinede birden fazla sunucu rolünü bağımsız sanal roller içerisinde çalıştırımamızı sağlayan Microsoft ürünü de diyebiliriz)  gibi isimler duymaya başladık. Bir başka deyişle sanallaştırma kavramları ile içli dışlı olmaya başladık.

Sanallaştırma sayesinde tek bir fiziki sunucu üzerinde farklı işletim sistemlerini konuşlandırabilmekte. Bu teknikle özellikle dağıtım süreçlerinin hızlandığını ve yeni fiziksel sunucular almak zorudan kalmadığımız için maliyet avantajları sağlandığını ifade edebiliriz. Pek tabii ölçekleme maliyetleri de azalıyor. Ancak her ziyaretçi işletim sistemi(Guest OS) için ayı bir işletim sistemi barındırmak durumunda da kalıyoruz ki bu negatif bir özellik olarak karşımıza çıkıyor. Diğer yandan uygulamalarının taşınabilirliği yeteri kadar esnek olmuyor. Diğer bir dezavantaj.

Derken karşımıza Docker diye bir şey çıktı. Go dili ile geliştirildiği söylenen bu yeni yaklaşımın özeti kabaca aşağıdaki şekilde görüldüğü gibi.

Docker gibi Container araçları sayesinde uygulamalarımızı sadece ihtiyaç duydukları kütüphaneler(paketler) ile birlikte birbirlerinden izole olacak şekilde çalışabilir halde sunabiliyoruz. Dağıtımın kolaylaşması dışında taşınabilirlik de kolaylaşıyor. En önemli artılarından birisi ise uygulamalara has çalışma zamanlarının birbirlerinden tam anlamıyla izole edilebiliyor olması.

Aşağıdaki şekil Docker'ın temel çalışma mimarisi özetlenmeye çalışılmakta. 

Docker temel olarak istemci-sunucu mimarisine uygun olarak geliştirilmiştir. GO dili ile yazıldığını sanıyorum belirtmiştik. Kullanıcılar esas itibariyle Docker Client üzerinden Demaon ile iletişim kuruyorlar. Build, Pull ve Run gibi komutlar Docker Client aracılığıyla, Deamon üzerinden işletilmekteler. Docker Demaon devamlı olarak çalışan bir Process(Sanırım Windows Service'e benzetebiliriz) Container’lar aslında birer çalışma zamanı nesnesi ve uygulamaların yürütülmesi için gerekli ne varsa(betikler, paketler vs) barındırıyorlar. Image öğeleri de Container’ların oluşturulması için kullanılan yalnızca okunabilir şablonlar olarak tasarlanmışlar. Şekilde Build, Pull ve Run operasyonlarının temel çalışma prensiplerini görebiliriz(Okların renklerine dikkat edelim)

Özetleyecek olursak

  • Docker Store : Güvenilir ve kurumsal seviyedeki imajların kayıt altına alındığı yer.
  • Docker Client : Deamon ile iletişim kuran komut satırı aracı.
  • Docker Deamon : Container'ların inşa edilmesi, çalıştırılması, dağıtılması gibi operasyonları üstlenen arka plan servisi.
  • Image : Uygulamalar için gerekli konfigurasyon ve dosya sistemi ayarlarını taşıyan ve Container'ların oluşturulması için kullanılan nesneler. Docker dünyasında base,child,official ve user tipinden imajlar bulunuyor. base tipindekiler tahmin edileceği üzere linux,macos,windows gibi OS imajları.child imajlar base'lerden türetilip zenginleştiriliyor. Docker'ın official imajları(pyhton, alpine, nginx vb) dışında kullanıcıların depoya aldığı docleağrulanmış imajlarda söz konusu.
  • Container : Image'ların çalışan birer nesne örneği. Bir Container çalışan uygulama için gerekli tüm bağımlılıkları bünyesinde barındırır. Kendi çekirdeğini(Kernel) diğer Container'lar ile de paylaşır. Tamamen izole edilmiş process üzerinde çalışır.

Docker'ın Kurulumu

Kurulum işlemlerinde halen tam olarak anlamadığım adımlar olsa da benim için önemli olan West-World'e Docker'ın başarılı bir şekilde yüklenmesiydi. Aşağıdaki adımları izleyerek bu işlemi gerçekleştirdim.

Her ihtimale karşı işe başlarken paket indeksini güncellemek gerekiyor.

sudo apt-get update

Sonrasında https üzerinden repository kullanımı için gerekli paket ilavelerinin yapılması lazım(Ben önceden yapmışım o yüzden yeni bir şey eklemedi)

sudo apt-get install apt-transport-https ca-certificates curl software-properties-common

Bu işlerin ardından Docker'ın (GNU Privacy Guard-gpg) anahtarının sisteme eklenmesi gerekiyor.

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

Docker dökümanına göre anahtarın kontrol edilmesinde yarar var. Harfiyen dediklerine uyuyorum ve FingerPrint bilgisinin son 8 değerine bakıyorum.

sudo apt-key fingerprint 0EBFCD88

Bilgi doğrulanıyor. Artık Repository'yi ekleyebilirim. West-World, Ubuntu'nun Xenail türevli işletim sistemine sahip. Bu sebeple uygun repo seçimi yapılmalı. Siz sisteminiz için uygun olan sürümü yüklemelisiniz(Diğer yandan production ortamlarında sürüm numarası belirterek de yükleme yapılabiliyor)

sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"

Kurulum öncesi yine paket indeksini güncellemekte yarar var. Nitekim docker repository paketleri yüklenecek.

sudo apt-get update

Bu ön hazırlıklardan sonra docker'ın kurulumuna başladım. Aşağıdaki komut ile sisteme Community Edition sürümü yüklendi.

sudo apt-get install docker-ce

Kurulumdan emin olmanın yolu standart bir imajı test etmekten geçiyor. Bunun için hello-world imajını kullanmamız öneriliyor.

sudo docker run hello-world

Tabii Docker yeni kurulduğu için hello-world imajı sistemde bulunmuyor. Bu yüzden run komutu sonrası ilgili paketin son sürümü indirilecek ve sonrasında da çalıştırılacaktır. "Hello from Docker!" cümlesini görmek yeterli.

Eğer adımlara dikkat edilecek olursa yukarıdaki çalışma şeklinde bahsedilen işlemlerin yapıldığı da görülebilir. İlk olarak Docker Client , Docker Deamon'a bağlanıyor. Sonrasında Deamon, hello-world imajını Hub'dan çekiyor(Hub'da sayısız imaj olduğunu belirtelim) İmaj çekildikten sonra bir Container oluşturulup çalıştırılıyor. Sonuçlar da istemciye sunuluyor. Artık West-World' de Docker kurulmuş vaziyette. Terminalde docker kullanımı ile ilgili daha fazla bili almak için --help anahtarını da kullanabiliriz.

docker --help

docker pull --help

gibi

Basit Bir .Net Core Console Application

Docker üzerinde host etmek için deneme amaçlı bir Console uygulaması yazarak devam etmeliyim. Terminalden aşağıdaki komutu kullanarak şanslı sayı üretmesini planladığım uygulamayı oluşturdum.

dotnet new console -o LuckyNum

Ardından Program.cs içeriğini aşağıdaki gibi değiştirdim.

using System;

namespace LuckyNum
{
    class Program
    {
        static void Main(string[] args)
        {
            Random randomizer=new Random();
            var num=randomizer.Next(1,100);
            Console.WriteLine("Merhaba\nBugünkü şanslı numaran\n{0}",num);
        }
    }
}

Program çalıştırıldığında bizim için rastgele bir sayı üretiyor. İçeriği aslında çok da önemli değil. Amacım uygulamayı Docker üzerinden yürütmek. İlerlemeden önce programın çalıştığından emin olmakta yarar var tabii.

Sıradaki adımsa uygulamanın publish edilmesi. Terminalden aşağıdaki komutu kullanarak bu işlem gerçekleştirilebilir.

dotnet publish

Sonuçta LuckyNum.dll ve diğer gerekli dosyalar bin/debug/netcoreapp2.0/publish klasörü altına gelmiş olmalı.

Console Uygulamasını Docker'a Almak

Nihayet son adıma geldim. Kodların olduğu klasöre gidip Dockerfile isimli bir dosya oluşturmak gerekiyor(Uzantısı olmayan bir dosya. DockerFile gibi değil Dockerfile şeklinde olmalı. Nitekim docker bunu ele alırken Case-sensitive hassasiyeti gösterdi. Epey bir deneme yapmak zorunda kaldım) Dosya içerisinde bir takım komutlar olacak. Bu komutlar aslında Linux temelli.

FROM microsoft/dotnet:2.0-sdk
WORKDIR /app

COPY /bin/Debug/netcoreapp2.0/publish/ .

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

Bütün Dockerfile içerikleri mutlaka FROM komutu ile başlar. Burada base image bilgisini veriyoruz ki örnekte bu microsoft hesabına ait dotnet:2.0-sdk oluyor. Dosyayı oluşturduktan sonra bir build işlemi gerçekleştirmek ve imajı inşa etmek lazım. 

sudo docker build -t lucky .

Bu komutla Deamon Hub üzerinden microsoft/dotnet:2.0-sdk imajı indirilmeye başlanacak. Dockerfile içerisindeki ilk satırda bunu ifade ediyoruz. Sonrasında basit dosya kopyalama işlemi yapılacak ve çalışam zamanındaki giriş noktası gösterilecek.

DotNet Tarafı için kullanılabilecek Docker Image listesine buradan ulaşabilirsiniz. Hem Linux hem Windows Container'ları için gerekli bilgiler yer alıyor.

Artık elimde lucky isimli bir imaj var. Bu imajı doğrudan çalıştırabileceğimiz gibi bu imajdan başka bir tane çıkartıp onu da yürütebiliriz. Aşağıdaki kodda bu işlem gerçekleştirilmekte. Tabii luckynumber'ın kalıcı olması için  commit işlemi uygulanması da gerekebilir. Docker'ın komutları ve neler yapılabileceği şimdilik yazının dışında kalıyor ama ara ara bakmaya çalışacağım.

Bu arada oluşturulan imajları isterseniz cloud.docker.com adresinden kendi hesabınızla da ilişkilendirebilirsiniz. Şu adresteki python örneğini adım adım yapın derim ;)

sudo docker run --name luckynumber lucky

Ben yazıyı hazırlarken bir kaç deneme yaptığım için Docker build işleminin çıktısı sizinkinden farklı olabilir. Nitekim dotnetcore imajının indirilmesi ile ilgili adımlar da yer almaktaydı. Sisteme yüklü olan imajların listesini de görebiliriz. Hatta kaldırmak istediklerimiz olursa rm veya rmi komutlarını da kullanabiliriz. Bunlar örneğe çalışırken işime yarayan komutlardı.

Docker'ın çalışma prensiplerini daha iyi kavramak ve örneklerle uygulamalı olarak onun felsefesini anlamak için GitHub üzerindeki şu adrese uğramanızı tavsiye ederim. 

West-World şimdi biraz daha mutlu. Çünkü .Net Core 2.0 ile yazılmış bir programı dockerize etmenin nasıl bir şey olduğunu öğrendi. Ben de tabii. Elbette docker'ın gücünü anlamak için farklı açılardan da bakmak gerekli. Söz gelimi official imajlardan olan python'un çekip üretilen container üzerinde doğrudan python ile kodlama yapmaya başlayabiliriz. Aşağıdaki ekran görüntüsüne dikkat edin. Sistem python yüklememize gerek yok. pyhton çalışmak için gerekli herşeyin yer aldığı bir imajı çekip başlatılan container üzerinden kodlama yapabiliriz.

Docker, .Net Core gibi konular önümüzdeki yıllarda geliştiricilerin iyi şekilde hakim olması gereken konular arasında yer alıyor. Vakit ayırıp planlı bir şekilde çalışmak lazım. Böylece geldik bir yazımızın daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Go Web Sunucusunu Docker Üzerinden Yayınlamak

$
0
0

Merhaba Arkadaşlar,

Gondor'da bir şeyler araştırmak için harika bir zaman. Çünkü elimdeki işler bitti. Böyle vakitleri kendi araştırmalarıma ayırmak hoşuma gidiyor, kim ne derse desin. Yeni gözdem Linux makinem de(Gondor)önümde durduğuna göre kısa bir süre onun üzerinde çalışabileceğimi düşünüyorum.

Aklıma gelen ilk şey ise, Go diliyle yazılmış ilkel bir web sunucusunu Docker üzerinden kullanabilmek. Önce web sunucusunu geliştirmek, başarılı bir şekilde çalıştığından emin olmak, sonrasında bir Docker imajı hazırlamak lazım. Ardından oluşturulan imajdan yararlanarak bir Container başlatıp web sunucusunun bu taşıyıcı örneği üzerinden çalışıp çalışmadığını test etmek senaryonun tamamlanması açısından yeterli. Tahminlerime göre 15 dakikayı aşmayacak bir iş gibi duruyor. Haydi başlayalım.

Gondor'da açtığım SimpleWebServer klasörüne aşağıdaki kod parçasını içeren main.go isimli bir dosya ekleyerek ilerliyorum. Tabii buradaki ortamda GOPATH tanımlamalarını değiştirmiştim. $home\goprojects altında konuşlandırıyorum (Geliştirici arabirimi olarak Visual Studio Code' tan faydalanıyorum. Her zaman ki gibi çok keyifli bir geliştirici deneyimi sunuyor. Size de tavsiye ederim)

package main

import(
    "fmt"
    "net/http"
    "runtime"
    "math/rand"
    "time"
)

func indexHandler(w http.ResponseWriter,r *http.Request){
    t:=time.Now()   
    w.Header().Set("Content-Type","text/html; charset=utf-8")
    fmt.Fprintf(w,"Gondor time : <b>(%s)</b><br/>We are running on <b>%s</b> with an <b>%s</b> CPU<br/><i>Your lucky number</i> <b>%d</b>",t.Format(time.RFC1123),runtime.GOOS,runtime.GOARCH,rand.Intn(100))
}

func main(){
    http.HandleFunc("/",indexHandler)
    fmt.Println("listening...")
    http.ListenAndServe(":8087",nil)    
}

Uzun zamandır Go ile kod yazmıyordum. Özlediğimi ifade edebilirim. Özellikle de kurallarını ve basitliğini. Ana paketteki programın başlangıç noktası olan main içerisinde HandleFunc isimli fonksiyondan yararlanarak root adrese gelecek olan talepleri indexHandler isimli operasyona yönlendiriyoruz. indexHandler içerisinde ise çok basit bir HTML içeriği bastırmaktayız. Elle tutulur bir şeyler olması açısından güncel zaman bilgisini, işletim sistemini, işlemcinin türevini yazdırdıktan sonra 0 ile 100 arasında üretilecek rastgele bir sayı da basıyoruz. İçeriğin HTML tipinden olacağını Header'a ait Set fonksiyonu ile belirtmekteyiz. Böylece istemciye ulaşacak paketin html olarak yorumlanılması gerektiğini de söylemiş oluyoruz. main içerisinde yer alan ListenAndServe fonksiyonu da 8087(istediğiniz bir portu kullanabilirsiniz tabii) nolu porttan yayın yapılmasını sağlamakta. 

İlk olarak kodun bu şekilde çalışıp çalışmadığını test etmek lazım. Terminalden 

go run main.go

komutunu vererek bu denemeyi yaşayabiliriz. Aşağıdaki ekran görüntüsünde örnek bir çalışma zamanına yer veriliyor. Ben bu görüntüyü aldığımda yine gülümsedim hafifçe.

Hedefimiz şu. Bu uygulamayı başlatıldığı zaman ayağa kaldıracak bir Docker imajı oluşturmak. Bunun yolu bildiğiniz gibi ilgili komutları içerecek bir Dockerfile oluşturmaktan geçiyor. main.go ile aynı lokasyona aşağıdaki içeriğe sahip docker dosyasını ekleyerek ilerleyebiliriz.

FROM golang

ADD . /go/src/GoWebServer
RUN go install GoWebServer
ENTRYPOINT [ "/go/bin/GoWebServer" ]

EXPOSE 8087

Öncelikle resmi golang imajından feyz aldığımızı belirtelim. İlk satırda bunu belirtmekteyiz. Sonrasında kaynak kodun tamamını oluşturulan imaj içerisindeki /go/src/GoWebServer adresine taşıyoruz. Tahmin edileceği üzere golang imajındaki GOROOT ve GOPATH ortam ayarlamalarına uyan taşımalar yapılmakta. Bu path tanımlamaları ata imajda hazırlanmış. GOPATH tanımına göre kaynak kodları go/src altına atmamız gerekiyor. Sonrasında GoWebServer içeriğini sisteme yüklüyoruz(Go Deployment) RUN ifadesinden sonra gelen komut bu işi yapmakta. Container başlatıldığında giriş noktası olarak kurulumun gerçekleştiği go/bin/GoWebServer klasörünü işaret ediyoruz. Son olarak Container'ı localhost:8087 portu üzerinden yayına açıyoruz.

Dosya hazırlandıktan sonra imajın oluşturulmasına başlayabiliriz. Bu tipik olarak Docker'ın Build operasyonu. Terminalden aşağıdaki komutu kullanarak inşayı başlatabiliriz.

sudo docker build -t go_rnd_server .

go_rnd_server isimli bir imaj oluşturduk. Eğer indirilmesi gereken içerikler varsa sisteme yüklenmesi için bir süre beklemek gerekebilir. Herhangibir hata alınmadıysa oluşturulan imajı kullanarak yeni bir Container başlatabiliriz. Bunun için docker'ın run komutundan yararlanmak gerekiyor. -p den sonra gelen adres ile yayınlamanın hangi adresten yapılacağını da belirtmiş oluyoruz.

sudo docker run -p 8087:8087 go_rnd_server

Ekran görüntüsünden de görüleceği gibi artık docker üzerinde konuşlandırdığımız Web sunucusuna gidebiliyoruz(Biraz daha gülümsüyorum)Örneği genişletmek tabii ki sizin elinizde. Burada Go ile yazılmış bir REST servis de sunulabilir. Hatta veri için Redis gibi bir yapı kullanılarak senaryo daha da heyecanlı hale getirilebilir(En sevdiğiniz filmdeki sözlerin içeren bir veri kümesinden kullanıcıya rastgele sözler yolladığınız bir örnek üzerinde çalışabilirsiniz) Bunlara ek olarak söz konusu Container'lardan bir kaçının farklı port'lardan başlatılması da denenebilir. Bu mümkün olabilir mi şimdilik bilmiyorum ama denemek de istiyorum. Özellikle ortak veri kullanımları veya verinin tüm Container'lar için eşleştirilmesi gibi epik senaryoları kafamda şekillendirmekte henüz zorlanıyorum.

Makaleme son vermeden önce çalışmakta olan Container'ı nasıl durduracağımızı da söyleyeyim. Eğer bir Deploy işlemi gerçekleştireceksek bunun öncesinde ilgili Container örneğinin durdurulması önerilmiş. İlk olarak var olan Container örneklerini görelim. Terminalden vereceğimiz aşağıdaki komutu bunu sağlayacaktır.

sudo docker ps -a

Çalışmakta olan bir Container örneğini durdurmak için de ID değerini kullanabiliriz. Aşağıdaki gibi.

sudo docker stop fb3936d9f595

Böylece geldik kısa süreli faydası olan bir öğle arasının daha sonuna. Artık işin sırrı öğrenildi diyebilirim. Hakim olduğunuz bir dille basit bir Web sunucusu veya REST servisi yazıp Docker ile sunmayı deneyebilirsiniz. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Apache Kafka ile Konuşmaya Çalışmak

$
0
0

Merhaba Arkadaşlar,

Bilgisayarla yeni tanıştığım dönemlerde aynı zamane çocuklarımızın tabletlerde yaptığı gibi oyun oynamaya bayılırdım. Dersler, sınavlar bir yana oyunlar bir yana. Kasetlerin olduğu Commodore 64 zamanlarından, disketler ile 486DX işlemcili makinelerdekilere, CD ile yüklenenlerden, internetten indirilip oynananlara kadar... Pek çok efsane vardı tabii oynananlar arasında. Bazen onların başında saatlerce nasıl vakit geçiriyormuşum diye düşününce hayret ediyorum kendime. Hoşuma giden oyunların en önemli özellikleri arasında ses efektleri ve karakter konuşmaları gelirdi.

Duke Nukem'da onlardan birisidir. Geçenlerde internetten sözlerini araştırırken 3D isimli sürümünün listesini bulup text dosya içine attım. Neme lazım belki bir kod parçasında rastgele söz çıkartmak için kullanırız(Hatta Docker'ın bir tutorial'ında kullanılmıştı diye biliyorum) Peki bugünkü konumuzda nerede ele alacağız? Önce West-World'de neler oluyor bir bakalım.

West-World oldukça hareketli günler geçiriyor. En son Docker ile ilgili denemelerim olmuştu. Doğruyu söylemek gerekirse Ubuntu işletim sisteminden ve üzerinde çalışmaktan oldukça memnunum. Visual Studio Code inanılmaz esnek bir ortam sağlıyor. Dosyaları uzantısından tanıyıp önerilerde bulunabiliyor. Geniş bir extension deposu var. Diğer yandan .Net Core'u keşfetmeye de devam ediyorum. Docker tarafına da zaman zaman dönüp bakmaktayım. Resmi sitesindeki öğretiler gerçekten keyifli ve ne işe yaradığını anlıyorsunuz. Dipnot olarak terminal üzerinde çalışmak da keyif veriyor. Konuyu özümsemek adına adımları daha net görebiliyorum. Ancak yapmam gereken çok şey var. Şimdilik merak ettiğim konuları denemeye gayret ediyorum. Bunlardan birisi de bir kaç aydır çalışmakta olduğum ekipte sözü geçen ve üzerinde denemeler yapılan Apache Kafka. Onu elasticsearch ile entegre edip büyüyen loglarımızı dizginlemek amacıyla kullanmayı planlıyorlar.

Aktif olarak kullanıldığından nasıl bir şeydir öğrenmem gerekiyordu. Önce bankanım tahsis ettiği Ubuntu dizüstü üzerinde denemeler yaptım. Sonrasında West-World ile konuyu pekiştirmeye çalıştım. Tabii her şeyden önce Kafka'nın felsefesini kavramam lazımdı. Internette konu ile ilgili derya deniz kaynak var. Benim için en akılda kalıcı ve anlamlı şeyse temize çektiğim aşağıdaki not oldu.

Şekildeki adımlar kısaca şöyle; Yayımcı/yayımcılar(Producer) belli bir konu başlığına(topic) ait mesaj/mesajlar yayınlar. Broker üzerinden akan mesaj başka bir tüketici/tüketiciler(Consumer) tarafından okunabilir. Karşımızda bir mesaj dağıtım sistemi var görüldüğü gibi. Kafka'nın yaptığı ise hasara uğramadan gerçek zamana oldukça yakın sürelerde veri akışkanlığını sağlamak. Sistem biraz daha netleşecek. Önemli sorulardan birisi neden böyle bir ürüne ihtiyaç duyulduğu? Verinin inanılmaz derecede büyüdüğünde hem fikiriz. Üstelik yıllardır var olan bir mevzu bu. Big Data kavramının ortaya koyduğu pek çok sorunsal da var. Verinin bu denli büyüyor olması bilgi akışlarının gerçek zamanlı olmasını negatif etkileyen bir durum. Kolayca ölçeklenebilir ve dağıtık modelde çalışabilecek sistemler gerekli.

İşte Kafka, vakti zamanında Linkedin'e çare olmuş bu sorunsalda. Sonrasında açık kaynak olarak sunulmuş(Tabii Linkedin ile sınırlı kalmamış. Paypal'den Twitter'a, Spotify'dan Netflix'e kadar pek çok tanıdık oyuncu var kullanıcıları arasında) Onun için başarılı bir dağıtık mesajlaşma servisi olduğunu ifade edebiliriz. Publisher/Subscriber(mesaj n sayıda tüketiciye gidebilir) ve Message Queue(Point to Point olarak da ifade ediliyor sanırım-mesaj sadece bir alıcı tarafından alınabilir) gibi iki modeli destekleyen, TCP/IP tabanlı, platform bağımsız, kolay ölçeklenebilir ve mesajları log kayıtlarına benzer yapıda tutan tam asenkron bir sistem var önümüzde.

Kafka özellikle büyük boyutta loglama yapılan sistemlerde çok değerli. Log hareketlilikleri ve mesaj içerikleri nedeniyle başa bela olabilen bankacılık sistemlerinde ilk kabul gören çözümler arasında yer alıyor. Bunların dışında Streaming, Event Sourcing, Message Queing, Web Activity Tracing gibi konu başlıklarında da değerlendirilmekte.

Burada dikkat edilmesi gereken önemli noktalardan birisi de Kafka'nın kullanım amacı. Büyük veriyi tutmak için değil bunları toplayıp ilgili sistemelere hatasız ve hızlı biçimde aktarmak için kullanılan bir mesajlaşma hizmeti olarak değerlendirmek daha doğru gibi. Bu sebeple çoğunlukla tek başına ele alınmamakta. Kafka'yı kullanarak verinin ElasticSearch, Hadoop, Spark gibi sistemlere akıtılması söz konusu. Bunun belli başlı motivasyon kaynakları var. Her şeyden önce ilgili verinin aktarılacağı sistemler kapalı olsa bile bir süre Kafka'da tutma imkanı bulunmakta. Bu yetenek uç sistemlerden birinin çökmesi durumunda mesaj kaybını da engellemekte. Diğer bir motivasyon sebebi de verinin büyüklüğü. Büyük veriyi diğer sistemlere taşırken paralel çalışabilen ölçeklenebilir bir dağıtık sistemin arada olması önemlidir.

"Aslında Kafka'yı anlamak için Arthur Schopenhauer'i anlamak lazım" demek isterdim ama esasında diğer Message Oriented Middleware'lere bakmamız gerekmekte. Microsoft Message Queue, IBM MQ, Apache Qpid, RabbitMQ ve varsa diğerleri. Bunlar Publisher/Subscriber modelini başarılı şekilde uygulayan MOM ürünleridir ancak akan veri büyüdüğünde ve Publisher sayısı onbinler seviyesine çıktığında otoriterlerin de belirttiği üzere çuvallarlar.

Kafka'yı onlardan ayıran unsulardan birisi Subscriber'ların mesajları nasıl aldığı ile ilgili. Kafka'da Consumer'ın mesajı alabilmesi için belli bir konu başlığına abone olması gerekir. Diğer yandan Kafka düşük veri kaybını garanti etmektedir. Hem real-time veri akışını sağlayabilir hem de offline çalışma yeteneği sayesinde veriyi saklayabilir.

Tabii işin özünde O da diğer MOM ürünlerinde olduğu gibi mesajları farklı bileşenler arasında taşımayı esas amaç edinmiştir.

Kafka ile konuşmaya başlarken bazı temel anahtar kelimelerine aşina olmak gerekiyor. Yukarıdaki şekilde de bahsettiğimiz gibi temel terimlerimiz Producer, Topic, Consumer ve Broker. Makaleye hazırlanırken işin mimari boyutlarına az da olsa girmeye çalıştım ancak kısa sürede odak noktamı kaybettim. West-World'teki amacım Kafka'yı kurup .Net Core kullanarak kendisiyle haberleşebilmekti en nihayetinde. Belli bir Topic için mesaj gönderip okuyabilirsem benim için yeterli olacaktı.

Önce Kurulum

Tabii işe ilk olarak Kafka'yı kurmakla başlamak lazım. Ben Kafka'yu doğrudan West-World üzerinde kuracağım ama dilersek Docker üzerinde de konuşlandırabiliriz(Bu aklımın bir köşesinde dursun) Sırasıyla aşağıdaki adımları takip ederek kurulum işlemini gerçekleştirebiliriz.

Alışılageldiği üzere adettendir bir update işlemi yapmakta yarar var.

sudo apt-get update

Java ortamına ihtiyacımız bulunuyor. O nedenle onu da kuruyoruz.

sudo add-apt-repository -y ppa:webupd8team/java
sudo apt-get update
sudo apt-get install oracle-java8-installer

Ardından ZooKeeper'ı yüklememiz gerekiyor. Kendisi Cluster'ları yönetecek olan koordinatör servisi oluyor. Kafka hizmete başlamadan önce onun başlatılması gerekiyor.

sudo apt-get install zookeeperd

ZooKeeper aslında dağıtık sistem koordinatörü olarak düşünülebilir. Söz gelimi Cluster'a bir Broker eklendiğinde ya da Broker çöktüğünde bağlı olan Producer ve Consumer nesnelerini bilgilendirmek gibi kritik görevleri üstlenir.

Artık Kafka kurulumuna başlanabilir. Kafka'nın sıkıştırılmış paketini sistemde açacağımız bir klasöre indirebiliriz. Ben Home dizininde Kafka isimli bir klasör içerisine indirdim. Sürüm farklılık gösterebilir. En güncel versiyonu öğrenmek için şu adrese bakmakta yarar var.

mkdir Kafka
cd Kafka
wget http://ftp.jaist.ac.jp/pub/apache/kafka/0.10.0.0/kafka_2.11-0.10.0.0.tgz
tar -xvf kafka_2.11-0.10.0.0.tgz -C .

Kafka'yı hemen deneyebiliriz. İlk olarak ZooKeeper hizmeti başlatılmalı.

sudo bin/zookeeper-server-start.sh config/zookeeper.properties

Bu arada başlatma sırasında bazı hatalar alınabilir. Örneğin ben Java Runtime'dan 2181 nolu portun kullanıldığına dair "Address already in use" hatası aldım ki bu port ZooKeeper tarafından kullanılmaktadır. O nedenle bir kill operasyonu gerçekleştirmem gerekti. 2181 nolu portun boşta olup olmadığını anlamak için 

sudo netstat -lnap | grep 2181

komutunu kullanabilirsiniz. Eğer sorun çıkmazsa ZooKeeper çalışmaya başlayacaktır.

Yaptığımız işlem bin klasörü altındaki zookeper-sever-start.sh betiğini config klasöründeki zookeper.properties dosyasında belirtilen varsayılan ayarlarla başlatmak. Şimdi kafka servisi etkinleştirilebilir. Bunun için yeni bir terminal penceresi açıp kafka paketini açtığımız konuma giderek aşağıdaki komutu çalıştırabiliriz.

sudo bin/kafka-server-start.sh config/server.properties

Terminal Penceresinde Eğlence Zamanı

Ne durumdayız? Koordinatör servisimiz etkin ve bir tane Kafka Broker örneği de çalışır vaziyette. Kafka sunucusu çalıştırırken config altındaki server.properites dosyasından yararlanılmakta. İstersek bunları çoğaltabilir içeriklerindeki port numaralarını farklılaştırarak aynı anda birden fazla Broker'ın çalışmasını da sağlayabiliriz. Bu tam anlamıyla bir gerçek hayat senaryosu olur. Benim amacım bu kadar uzun boylu değil. Tek broker nesnesi üzerinden birer Producer(Publisher oluyor) ve Consumer(Subscriber oluyor) arasında mesaj dolaştırsam yeterli. Bunu yapmak içinse öncelikle bir konu başlığı(topic) açmamız gerekiyor. Yeni bir terminal penceresi açalım ve Producer rolünde bir topic açıp içerisine bir kaç bilgi atalım.

sudo bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic ToDoList

sudo bin/kafka-topics.sh --list --zookeeper localhost:2181

kafka-topics.sh betiğinden yararlanarak ilk komutla ToDoList isimli bir topic oluşturuyor, sonraki komutla da var olanları listeliyoruz. create ve list komutlarını kullanırken zookeeper adresini belirttiğimize dikkat edelim. Sanırım birden fazla Cluster olduğu senaryolarda birden fazla ZooKeeper hizmetinden yararlanabiliriz. Bu örnekte tek Broker kullanıldığı için replication faktörü 1 olarak belirlendi. Şimdi bir Producer oluşturup ToDoList isimli Topic altına birkaç veri ekleyelim.

sudo bin/kafka-console-producer.sh --broker-list localhost:9092 --topic ToDoList

Bu komutu çalıştırdığımızda terminal penceresinde satır bazlı veri girebilir hale geliriz. Aşağıdaki ekran görüntüsünde birkaç ToDo maddesi eklendiğini görebilirsiniz. Test için iki Consumer penceresi var. Consumer'ların bir Topic içeriğini görmesi için kafka-console-consumer.sh betiğinden yararlanmamız yeterli.

sudo bin/kafka-console-consumer.sh --zookeeper localhost:2181 --topic ToDoList --from-beginning

Örnekleri denerken ilgimi çeken noktalardan birisi de Producer penceresi açıkken girilen bilgilerin bir alt satıra geçildiğinde otomatik olarak abone olan tüm Consumer pencerelerine yansımasıydı. Bunu canlı izlemek çok keyifli bir deneyim. Mutlaka deneyin derim :)

Denemeler yaparken düştüğüm bir hata da vardı. Producer betiğini çalıştırdıktan sonra terminalde bir şeyler olmasını beklerken rastgele tuşlara basmış, boş satırlar eklemiştim. Bunlar mesaj olarak gönderilmiyor diye düşünüyordum, çünkü ekranda bir tepki oluşmamıştı. Ancak sonradan Consumer üzerinden ne eklediysem gördüm. Kirli bir ToDoList konusu ile çalışmak istemiyordum. Bu nedenle ilgili Topic nesnesini aşağıdaki komutla silmek istedim.

sudo bin/kafka-topics.sh --delete --zookeeper localhost:2181 --topic ToDoList

ToDoList isimli topic nesnesinin silinmek üzere işaretlendiğine dair bir mesaj aldım. Ancak topic listesini çektiğimde olduğu yerde duruyordu. Aynı mesajda silme operasyonunun etkili olması için konfigurasyonda delete.topic.enable özelliğinin değerinin true olması gerektiği de belirtilmişti. Yapılması gereken şey config klasöründeki server.properties dosyasının sonuna bu bildirimi eklemek ve Kafka sunucusunu tekrardan çalıştırmaktan ibaretti. Bunu yaptıktan sonra delete betiğini tekrar çalıştırdım ve ToDoList'in kaldırıldığını gördüm. Pek tabii o sırada bu konu başlığına bağlı olan aboneler varsa onlara ilgili Topic nesnesinin olmadığına dair bir hata mesajı gönderildi.

Kafka'yı terminalden az çok nasıl kullanacağımı öğrendim. Ancak terminalden uzun uzun o betikleri yazmaya çalışmak zevkli olsa da zorlayıcıydı. Her şeyden öte B12siz bir insanım. Çabuk unutuyorum. Şöyle işleri kolaylaştıracak kendi bildiğim dillerle kullanabileceğim bir API olsa hiç fena olmazdı. Hazır .Net Core dünyasında bir şeyler yapmaya çalışıyorken onunla ilerleyeyim dedim.

.Net Core Uygulamalarının Yazılması

Senaryomuzda iki Console uygulaması olacak. Birisinden Producer yardımıyla Broker'a mesaj bırakacağız. Diğer Console uygulamasını ise Consumer olarak tasarlayacağız. Consumer uygulaması, Producer tarafından belli konu başlıklarında bırakılan mesajları okumak için kullanılacak. İlk olarak FabrikamProducer'ı tasarlayalım.

dotnet new console -o FabrikamProducer

Bu işlemin ardından Confulent.Kafka paketini projemize eklememiz gerekiyor. Bu paket aracılığıyla Kafka ile konuşabileceğiz. Restore işlemini yapmayı ihmal etmessek iyi olur. Nitekim indirilen paket .Net Core 2.0 için yeniden ayarlanacaktır.

dotnet add package Confluent.Kafka
dotnet restore

Şimdi Program.cs içeriğini aşağıdaki gibi güncelleyelim.

using System;
using System.IO;
using System.Text;
using System.Collections.Generic;
using Confluent.Kafka;
using Confluent.Kafka.Serialization;

namespace FabrikamProducer
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Producer Tarafı\n");
            string brokerEndpoint="localhost:9092";
            var quotes=File.ReadAllLines("Quotes.txt");
            Random random=new Random();
            Console.WriteLine("Bir topic adı girer misin?");
            string topicName=Console.ReadLine();

            var config=new Dictionary<string,object>{
                {"bootstrap.servers",brokerEndpoint}
            };

            using(var producer= new Producer<Null,string>(config,null,new StringSerializer(Encoding.UTF8)))
            {
                for(;;)
                {                       
                    string message=quotes[random.Next(1,quotes.Length)-1];
                    var result=producer.ProduceAsync(topicName,null,message).GetAwaiter().GetResult();
                    Console.WriteLine($"Partition : {result.Partition} Offset : {result.Offset}\n{message}");                        
                    System.Threading.Thread.Sleep(10000);
                }
            }
        }
    }
}

Teori değişmiyor. Producer nesnesinin işini yapabilmesi için localhost:9092 adresindeki Broker ile konuşabilmesi gerekiyor. Bunun için config isimli değişkende bootstrap bilgisini ve endPoint adresini veriyoruz. Broker'a mesaj gönderme işini Producer tipinden bir nesne örneği gerçekleştirmekte. Mesaj string tipte ve UTF8 kodlamasına göre gönderilecek. Gönderme işini ProduceAsync isimli fonksiyon gerçekleştiriyor. İlk parametre ile kullanıcıdan aldığımı Topic adını belirtiyoruz. Son parametresinde ise Quotes.txt dosyasından çektiğimiz rastgele bir Duke Nukem sözü :) Eğer mesaj gönderimi başarılı ise bu mesajın Broker üzerindeki Partition ve Offset bilgilerini de ekrana basıyoruz. FabrikamProducer, 10 saniyede bir Duke Nukem mesajı yayınlayacak şekilde tasarlanmış durumda.

Abone olacak Console uygulamamızı ise FabrikamConsumer ismiyle oluşturabiliriz. Yukarıdakine benzer şekilde dotnet komutlarını kullanarak terminalden gerekli oluşturma işlemlerini yapıp, Confluent.Kafka paketini projeye dahil etmemiz gerekiyor.

dotnet new console -o FabrikamConsumer

dotnet add package Confluent.Kafka
dotnet restore

Sonrasında Program.cs içeriğini aşağıdaki gibi güncelleyebiliriz.

using System;
using System.Text;
using System.Collections.Generic;
using Confluent.Kafka;
using Confluent.Kafka.Serialization;

namespace FabrikamConsumer
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Consumer Tarafı\n");
            string brokerEndpoint="localhost:9092";
            Console.WriteLine("Hangi konu başlığına abone olacaksın?");
            string topicName=Console.ReadLine();

            var config=new Dictionary<string,object>{
                {"group.id","FabrikamConsumer"},
                {"bootstrap.servers",brokerEndpoint},
            };

            using(var consumer=new Consumer<Null,string>(config,null,new StringDeserializer(Encoding.UTF8)))
            {
                consumer.OnMessage+=(o,m)=>{
                    Console.WriteLine($"Duke Nukem diyor ki: {m.Value}");
                };

                consumer.Subscribe(new List<string>(){topicName});
                var isCancelled=false;
                Console.CancelKeyPress+=(_,e)=>{
                    e.Cancel=true;
                    isCancelled=true;
                };
                Console.WriteLine("Ctrl-C ile çıkabilirsin");
                while(!isCancelled)
                {
                    consumer.Poll(100);
                }
            }
        }
    }
}

Consumer nesnesi örneklenirken diğer örnekte olduğu gibi konuşacağımız Kafka Broker'ını belirtmemiz lazım. Bunun için yine localhost:9092 adresine bootstrap.servers ayarları ile gitmeye çalışıyoruz. Consumer nesne örneğinin OnMessage isimli olay metodu, abone olunan konu başlığına bir mesaj geldiğinde otomatik olarak tetiklenmekte. Bu nedenle ilgili olay metodu üzerinden m.Value ile ilgili Topic altına gelen metinsel bilgiyi(yani Duke Nukem sözünü) yakalıyoruz. Aboneyi ayakta tutmak için kullanıcı Ctrl+C tuşuna basana kadar uygulamayı bekletiyoruz. Poll operasyonunda yeni mesaj almaya hazır olduğumuzu belirtiyoruz. Normalde burada bir timeout süresi veriliyor ancak -1 ile bunu sonsuz olarak belirlemiş bulunmaktayız.

Uygulamaları test etmek için Visual Studio Code ortamındaki Integrated Terminal penceresini kullanarak dotnet run dememiz yeterli. Elbette Kafka sunucusunun ve onun öncesinde de ZooKeeper kontrol servisinin çalışır olduğundan emin olmalıyız. Sonuçlar benim için çok heyecan verici oldu. Açıkçası denemeniz ve kendi gözünüzle görmeniz lazım. 10 saniyede bir DukeNukem konu başlığına atılan mesajları, kaç tane abone varsa anında alabildi. İlginç, değişik, güzel, hoş. 

Producer çalışmasından bir görüntü

ve Consumer çalışmasından bir görüntü

Böylece amacıma ulaşmış oluyorum. West-World artık Kafka'nın felsefesine biraz daha aşina gibi. Tabii merak edilesi bir konu daha var. Acaba buraya elasticsearch nasıl bağlanıyor. Bakalım buna vakit ayırabilecek miyim. Böylece geldik bir makalemizin daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.


JWT(JSON Web Token) Kullanımı

$
0
0

Merhaba Arkadaşlar,

Daha önce söylemiş miydim bilemiyorum ama servis odaklı yaklaşımlarda güvenlik hep korktuğum ve anlamakta güçlük çektiğim konuların başında gelir. Özellikle WCF tarafındaki güvenlik senaryolarının çeşitliliği ve zenginliği bazen kafa karıştırıcı boyutlarda olabiliyor. Bu aralar şirketteki REST tabanlı servislerin JSON Web Token ile kullanılmalarına dair bir vaka çalışması söz konusu. Bu durum REST modelinde çalışan WCF servisleri için önemli.

Evdeki West-World'de ise aynı mevzu Asp.Net Core 2.0 Web API servisleri için gündeme geldi(Birkaç gün önce) Aslında o tarafta nasıl kullanıldığını merak etmek benim işim diyebilirim. Öğrenmekten keyif alacağım bir başka konu diyerekten geç vakitte çalışma odama geçtim. Beyin hücrelerine zarar verdiği söylenen sarı gece lambamı açtım. Spotify'da kısık tonda çalacak şekilde ayarladığım piyano tınıları ağırlıklı konsantrasyon albümünü başlattım. Sıcak kahvemden bir yudum aldım ve tarayıcıda aramalara başladım. İlk olarak JSON Web Token ne demektir bulduğum kaynaklardan anlamaya çalıştım. Sonrasında aşağıdakine benzer bir şeyler karalamayı da başardım.

Senaryo üzerinden gidildiğinde olay biraz daha basitleşmişti. REST tabanlı servisteki bir veya daha fazla operasyonu kullanmak isteyen bir kullanıcı olduğunu düşünelim. Kullanıcı hizmeti almadan önce username ve password bilgilerini de kullanarak doğrulanmakta(Authentication safhası diyelim) Doğrulama başlı başına büyük bir iş de olabilir. Microsoft Identity Server'dan tutun da Facebook doğrulamasına kadar farklı bir katman söz konusu esasında. Benim senaryomda bu kısım hep true olarak geçilecek ama bir gerçek hayat vakasında şirketin kimlik doğrulama sunucusundan veya Azure AD'den yararlanılabilir. Doğrulama işlemi başarılı olursa servis tarafında bir Token(bilet mi desek)üretilir. Bu Token konumuz gereği JSON Web Token tipinden olabilir.

Kısaca JSON Web Token 

JSON Web Token için ayrı bir parantez açmamız şart tabii. Yapısı 3 parçadan oluşan base64 formatında kodlanmış bir biletten bahsediyoruz. Sırasıyla Header, Payload ve Signature kısımlarından oluşmakta. Header kısmında biletin tipi ve şifreleme algoritmasına ait bilgiler yer alır. Payload içerisinde ise Claim olarak isimlendirdiğimiz diğer başka bilgilere bulunur. EMail, UniqueID, Certificate, Username vb veri içeren bir çok Claim aynı Token içerisinde yer alabilir.

Aslında JWT'nin kabul görmüş standartlarında yer alan Claim tiplerini koyabiliriz. Bu Claim'ler standartlara göre rezerve edilmiş tiplerden ya da Public, Private türünden oluşabilirler. Signature adı verilen son parça ise header, payload, şifreleme için kullanılacak gizli ifade ve şifreleme algoritmasının katkılarıyla üretilir. Var olan simetrik şifreleme algoritmalarından birisini tercih edebiliriz. base64 formatında oluşan bu üç parça aralarında noktalar içerek şekilde oluşur. Aslında örnek uygulama tamamlandığında nasıl bir şey olduğunu göreceğiz. 

Token üretimi, doğrulanan kullanıcının bir sonraki talebi için önemlidir. Örneğin HTTP Get ile geleceği bir operasyon için Authorization Header bilgisi olarak {Bearer token} şeklinde kullanılması gerekiyor. Bir başka deyişle doğrulanan kullanıcı için sonraki operasyonlarında kullanacağı biletin, bilet için biçilen yaşam süresi sonlanana kadar istemciden servis tarafına gönderilmesi gerekmekte. Kabaca senaryo bu şekilde çalışmakta. Süre dolduktan sonra kullanıcı aynı bilet ile içeriye giremez ve HTTP 401 hatası alır. Bu nedenle yeni bir bilet alması gerekir.

Aslında Bearer olarak isimlendirilen Token Based Authentication ve Authorization modelindeki güvenlik senaryoloarı sıklıkla kullanılmaktadır. OAuth 2.0 içerisinde de benzer bir teoriyi görebilirsiniz. Peki .Net Core 2.0 ile geliştirdiğimiz bir Web API'de, JSON Web Token tipinden biletleri nasıl kullandırabiliriz? Haydi gelin işe başlayalım.

Kod Tarafı

Öncelikle basit bir Web API uygulaması oluşturarak işe başlamamız gerekiyor. Örneği West-World'de geliştirdiğimizi hatırlatalım. Ubuntu 16.04 üzerinde .Net Core 2.0 kullanıyoruz.

dotnet new webapi -o HelloJWT

Authentication Middleware Ayarlamaları

Şimdi Startup dosyasının içeriğini aşağıdaki gibi geliştirelim.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;

namespace HelloJWT
{
    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=JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme=JwtBearerDefaults.AuthenticationScheme;
                }
            )
            .AddJwtBearer(options=>{
                options.TokenValidationParameters=new TokenValidationParameters{
                    ValidateAudience=true,                    
                    ValidAudience="heimdall.fabrikam.com",
                    ValidateIssuer=true,
                    ValidIssuer="west-world.fabrikam.com",
                    ValidateLifetime=true,
                    ValidateIssuerSigningKey=true,                    
                    IssuerSigningKey=new SymmetricSecurityKey(
                        Encoding.UTF8.GetBytes("uzun ince bir yoldayım şarkısını buradan tüm sevdiklerime hediye etmek istiyorum mümkün müdür acaba?"))
                };

                options.Events=new JwtBearerEvents{
                    OnTokenValidated=ctx=>{
                        //Gerekirse burada gelen token içerisindeki çeşitli bilgilere göre doğrulam yapılabilir.
                        return Task.CompletedTask;
                    },
                    OnAuthenticationFailed=ctx=>{
                        Console.WriteLine("Exception:{0}",ctx.Exception.Message);
                        return Task.CompletedTask;
                    }                  
                };
            });     
        }
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseAuthentication();

            app.UseMvc();
        }
    }
}

ConfigureServices içeriği oldukça önemli. İlk olarak doğrulama şemalarının eklendiğini görüyoruz. AddAuthentication metodunun ilk parametresinde bu tanımlamalar yapılmakta. AddJwtBearer metodundaki options parametresinin bir kaç özelliği belirlenmekte. Token için Auidence, Issuer ve SymmetricKey gibi bilgiler söz konusu. Diğer yandan bir Token doğrulandığında veya çalışma zamanı istisnası oluştuğunda devreye giren iki olay metodumuz da var. Örneğin eskimiş bir Token ile gelindiğinde OnAuthenticationFailed metodu devreye girecektir. ConfigureServices içerisinde tanımlanan bu doğrulama tekniğinin devreye alınabilmesi için Configure fonksiyonunda app.UseAuthentication çağrısının yapılması gerekiyor.

Controller Tipleri

Controller klasörüne iki sınıf ekleyeceğiz. Birisi Token üretimi işini üstlenecek. Bunu herkese açacağımız bir servis olarak düşünebiliriz. Bu nedenle AllowAnonymous niteliği ile imzalanabilir. Ana görevi gelen kullanıcı için bir token bilgisi üretip geri vermek. Diğer Controller tipimiz ise örnek bir operasyonu geçerli bir token için gerçekleştirmek üzere kullanılacak. Önce TokenController sınıfımızı yazalım.

using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;

namespace HelloJWT
{
    [Route("token")]
    [AllowAnonymous()]
    public class TokenController
        :Controller
        {
            [HttpPost("new")]
            public IActionResult GetToken([FromBody]UserInfo user)
            {
                Console.WriteLine("User name:{0}",user.Username);
                Console.WriteLine("Password:{0}",user.Password);

                if(IsValidUserAndPassword(user.Username,user.Password))
                    return new ObjectResult(GenerateToken(user.Username));
                    
                return Unauthorized();
            }

        private string GenerateToken(string userName)
        {
            var someClaims=new Claim[]{
                new Claim(JwtRegisteredClaimNames.UniqueName,userName),
                new Claim(JwtRegisteredClaimNames.Email,"heimdall@mail.com"),
                new Claim(JwtRegisteredClaimNames.NameId,Guid.NewGuid().ToString())
            };

            SecurityKey securityKey=new SymmetricSecurityKey(Encoding.UTF8.GetBytes("uzun ince bir yoldayım şarkısını buradan tüm sevdiklerime hediye etmek istiyorum mümkün müdür acaba?"));
            var token=new JwtSecurityToken(
                issuer:"west-world.fabrikam.com",
                audience:"heimdall.fabrikam.com",
                claims:someClaims,
                expires:DateTime.Now.AddMinutes(3),
                signingCredentials:new SigningCredentials(securityKey,SecurityAlgorithms.HmacSha256)
            );

            return new JwtSecurityTokenHandler().WriteToken(token);
        }

        private bool IsValidUserAndPassword(string userName, string password)
        {
            //Sürekli true dönüyor. Normalde bir Identity mekanizması ile entegre etmemiz lazım.
            return true;
        }
    }
}

Senaryomuzda harici bir Identity enstrümanı kullanmadığımız için IsValidUserAndPassword fonksiyonu daima true dönmekte. GenerateToken fonksiyonu bir kullanıcı için gerekli JSON Token içeriğini üretmekle görevli. PayLoad içerisine alınacak Claim bilgisi için someClaims değişkeninden yaralanılmakta. İçerisinde UniqueName, Email ve NameId gibi önceden tanımlı bilgiler bulunuyor.

Token'ın Signature içeriğinde kullanılmak üzere bir SecuritKey değişkeni kullanıyoruz. JwtSecurityToken nesnesini örneklerken de bazı bilgilere veriyoruz. Hatırlarsanız Authentication Middleware'ini ayarlarken Issuer, Audience, ExpireTime gibi bilgilerin gerekliliğinden bahsettik. Bu bilgileri yapıcı metoda parametre olarak girmekteyiz. issuer, audience, claims, expires, ve signingCredentials.

Şifreleme algoritması olarak genellikle Sha256 kullanılmakta. Bende geleneği bozmadım. Bu arada IActionResult döndüren GetToken operasyonu, istemcinin Body ile gönderdiği UserInfo bilgisini doğrulamak için kullanmakta(Body'den geleceğini belirtmek için [FromBody] niteliğini kullandık) UserInfo sınıfında Username ve Password özellikleri yer alıyor. Oluşturulan Token'ın dönüş içeriğine yazılması içinse WriteToken metodu çağırılmakta.

UserInfo yardımcı sınıfımıza ait kodlar;

namespace HelloJWT
{
    public class UserInfo
    {
        public string Username { get; set; }
        public string Password { get; set; }
    }
}

Son olarak PromotionController sınıfını yazarak işlemlerimize devam edelim. 

using System;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace HelloJWT
{
    [Authorize()]
    [Route("promotion")]
    public class PromotionController
        :Controller
    {
        [HttpGet("new")]
        public IActionResult Get() {
            foreach(var claim in HttpContext.User.Claims)
            {
                Console.WriteLine("Claim Type: {0}:\nClaim Value:{1}\n",claim.Type,claim.Value);
            }
            var promotionCode=Guid.NewGuid();
            return new ObjectResult($"Your promotion code is {promotionCode}");
        }
    }
}

Get isimli operasyon içerisinde o anki kullanıcıya ait Claim bilgilerinin ekrana bastırılması söz konusu. Bunu tamamen sunucu tarafındaki servise gelen token içeriğini izlemek amacıyla ekledik. Dönüş olarak istemciye şanslı promosyon kodunu göndermekteyiz. Bu promosyon kodu ile istediği marketten 50 liralık bedava alışveriş yapabilecek :P 

Aslına bakarsanız kodlarımız hazır. Şimdi test sürüşüne çıkabiliriz. Ne varki saat epeyce ilerlemiş durumda. Testlere ertesi gün Gondor'da devam etmek zorundayım(Gondor şirketteki Ubuntu)

Testler

Testleri yapmak için bir istemci uygulama yazabileceğimiz gibi Chrome'da Postman veya Firefox'ta HttpRequester gibi araçları da kullanabiliriz. Ben Firefox'a kurduğum HttpRequester'dan yaralanacağım. İlk olarak geçerli kullanıcı adı ve şifre ile bir bilet almamız gerekiyor. Bunu alırken body kısmında JSON formatında username ve password bilgisinin gönderilmesi önemli. HTTP POST tipinden bir talep gerçekleştireceğiz.

adres : http://localhost:5000/token/new

method : HTTP POST

content type : application/json

content : 

{
   "username":"Heimdall",
   "password":"P@ssw0rd"
}

Artık elimizde bir token bilgisi var. Bu token bilgisini kullanarak promosyon kodu için talepte bulunabiliriz. Burada da önemli olan Authorization isimli Header bilgisine Bearer {tokenbilgisi} şeklinde değer vermemiz. Geçerli ve henüz ölmemiş bir bilet vermemiz gerekiyor.

adres : http://localhost:5000/promotion/new

method : HTTP Get

header için key : Authorization

header için value : Bearer {token içeriği}

İşte çalışma zamanı.

Console'a gönderdiğimiz bilgilerin bir kısmına ait görüntüde aşağıdaki gibi.

Şimdi verdiğimiz zaman aşımı süresinin dolması sonrasında neler olacağına bakalım. Bu durumda token ömrü sonlandığı için hata almamız gerekiyor. Token bilgisinin geçersiz olması kullanıcıya 401 Unauthorized hatasını döndürür.

Halen daha kafamda soru işaretleri bulunmakta. Bunlardan birisi sizin de yapacağınız denelemelerde fark edeceğiniz üzere 3 dakikadan daha uzun sürede 401 almış olmamız. Ben denemelerimde bu süreyi tutturmayı bir türlü başaramadım. Yaptığım araştırmalarda Token doğrulaması için delegate edilen fonksiyonelliği ezebileceğimiz, AddJwtBearer metodundaki seçeneklerde ClockSkew özelliği için TimeSpan.Zero gibi değerler kullanabileceğimize dair ipuçları vardı. Belki de sorun Gondor'dadır. West-World üzerinden henüz kodları test edemedim ama şimdilik tek sorun geç dolan Token Timeout süresi gibi.

Esas itibariyle JSON Web Token, OAuth 2.0 alt yapısında kullanılan bir erişim standardı(Access_Token) Bu anlamda Bearer adı verilen Token türevinden de farklılaşıyor. JWT için Internet Engineering Task Force'un şu adresteki tanımlamalarına bakmakta fayda var. Bearer token tanımlamaları da bu adresten incelenebilir. Aralarında belirgin farklılıklar olduğu kesin. Doğruyu söylemek gerekirse araştırmaya devam ediyorum. Böylece geldik bir makalemizin daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Asp.Net Core Web API için Custom MiddleWare Yazmak

$
0
0

Merhaba Arkadaşlar,

Uzun zamandır televizyon dizisi izlemiyorum. Aslında bir dönem düzenli olarak takip ettiğim diziler vardı. Bir tanesi de usta oyuncular Benedict Cumberbatch(Sherlock Holmes) ve Martin Freeman(Dr. John Watson) ın oynadığı Sherlock Holmes idi. Bu Cumartesi gecesi bir şekilde dizinin bir bölümüne rastladım. Keyifli bir bölüm tekrarı yaptım. Oyunculuklara yine hayran kaldım. Sherlock'un keskin zekasına, Watson'un her zamanki sorgulayıcı düşünce tarzının eklendiği bir bölümdü.

Ardından kahvemi alıp West-World'e doğru yola çıktım. Bu kez Sergei Rachmaninof, Henry Auguste ve Dan Hawkins'in aralarında yer aldığı piyano tınıları eşliğindeydim. Saatler gece yarısını geçeli bir kaç dakika olmuştu. Çantamı açtım ve hafta içi .Net Core tarafındaki ara katmanın(Middleware) ne işe yaradığını anlamaya çalışırken karaladığım notlarımı buldum. Bilgilerimi toparlamanın ve güzel bir örnek yapıp tekrar unutmamak üzere bloğuma bir şeyler yazmanın tam vaktiydi. 

.Net Core açık kaynak olarak geliştirilmiş Kestrel web sunucusunu kullanmakta. Kestrel, web çalışma zamanının ayağa kaldırılmasından, istemciden gelen taleplerin(Request) hat üzerindeki ara katman modüllerine(pipeline middleware) geçirilmesinden ve tabii ki üretilen cevabın tekrar istemciye döndürülmesinden sorumlu(En temel fonksiyonellikleri bunlar) IIS gibi geniş kabiliyet ve yönetsel özelliklere sahip görünmese de bence çarpraz platformlarda microservice mimarisi için ideal bir çatı sunucusu. Benim ilgimi çekense Pipeline Middleware tarafı. Burada, seçilen web projesine göre varsayılan olarak gelen ara katman modülleri zaten mevcut. Peki kendi middleware tipimizi de sisteme dahil edebilir miyiz? (Saçma bir soru. Tabii ki de edebilirsin Burak; ama nasıl?) O zaman gelin bu işi nasıl yapabileceğimize bir bakalım.

Başlangıçta aşağıdaki terminal komutunu kullanarak standart bir Web API projesi oluşturarak işe başlayabiliriz. Örneğimizdeki amacımız orta katmana yeni bir arabirim ekleyebilmek. Bu nedenle ValuesController yapısını olduğu gibi bırakabiliriz. 

dotnet new webapi -o CustomMW

Asp.Net Core tarafında oluşturulan projeler bildiğiniz gibi standart bir takım kodlarla gelmekte. Startup.cs içeriği bu açından önemlidir. Nitekim web sunucusu ayağa kalkarken gerçekleştirilecek bir çok ön hazırlık ve çalışma zamanı hareketliliklerine ait işleyişler burada ele alınır. Configure metodunda yer alan app değişkeni, IApplicationBuilder arayüzünün oldukça şık bir uyarlamasını kullanır ve bir çok yeni ara katman özelliğinin çalışma zamanına dahil edilebilmesine olanak sağlar. Neler olduklarını görmek için Use ön eki ile başlayan metodlara bakmamız yeterli.

Dilersek ara katmanda devreye girebilir ve pipeline üzerinden hareket eden mesajları yakalayabiliriz(Aslında pek çok kaynakta ilk yapılan örnek bu) Bunun en basit yolu belli bir tipe bağlı olmadan kullanılabilen Use fonksiyonunu yazmaktan geçmekte. Aşağıdaki örnek kod parçasını ele alaım.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.Use(async (HttpContext ctx,Func<Task> next)=>{
        var request=ctx.Request;
        Console.WriteLine("REQUEST {0}",DateTime.Now.ToLongTimeString());
        Console.WriteLine("{0}-{1}{2}",request.Method,request.Host,request.Path);

        await next.Invoke();

        var response=ctx.Response;
        Console.WriteLine("RESPONSE:{0}",DateTime.Now.ToLongTimeString());
        Console.WriteLine("{0}\t({1}){2}",response.ContentType,response.StatusCode, (HttpStatusCode)response.StatusCode);
    });

    app.UseMvc();

}

Use metodu HttpContext ve Func<Task> tipinden parametreler alan metodları işaret edebilecek bir temsilci(delegate) ile çalışır. HttpContext ile web çalışma zamanına gelen ve giden içerikleri kontrol edebiliriz. Örnekte gelen mesaj ile ilgili HTTP metodu, adres bilgisi gibi değerler çekilip Console penceresine yazdırılmaktadır. Sonrasında işleyişin sıradaki ara katman hattına devredilmesi sağlanır(Invoke çağırımı) Ardından istemciye gönderilecek cevaba ait bir takım bilgiler yazdırılır. Örneğin dönen içeriğin tipi ve durum kodu bilgisi gibi. Çalışma zamanında yapılan denemelerin sonuçlarından örnek bir ekran görüntüsü aşağıdaki gibidir.

Dikkat edileceği üzere yapılan her talebe ilişkin bir takım bilgiler terminal penceresine yazılmıştır. IApplicationBuilder arayüzü üzerinden Use operasyonunu kullanılarak ilerlenilmesi basit ve pratik bir yol sunsa da, UseBlaBlaBla kullanımı kadar doğru değildir. Ara katmana dahil etmek istediğimiz operasyonları bir sınıf ile ilişkilendirmek sorumluluğun doğru yöne alınması ve tekrarlı kodların önlenmesi açısından iyi bir tercih olacaktır(Hatta siz ayrı bir Class Library içerisine alarak ilerleseniz daha iyi olur)Öyleyse yazının en heyecan verici kısmına başlayalım. 

Senaryomuz oldukça anlamsız ama olsun. En nihayetinde varmak istediğimiz nokta app.UseWatson gibi bir ara katman modülünü boru hattına entegre edebilmek. Ben örnek olarak HTTP Post ile gelen mesajların içerik boyutlarını kontrol etmeyi ve belli bir sınırın üstünde olmaları halinde terminal penceresine uyarı çıkartmayı planlıyorum. Örneğin yeni bir value eklenmek istendiğinde istemcinin JSON formatında bir içerik göndermesi gerekiyor. Bu içeriğin boyutunun kontrolünü şüpheci Watson'a verebiliriz. Pek tabii bir gerçek hayat senaryosunda talebin doğrudan NotAllowed gibi bir durum koduna çekilerek reddedilmesi de sağlanabilir. 

Senaryoda kritik olan noktalardan birisi mevzubahis içerik limitinin bir yerlerden alınması gerekliliği. Kuvvetle muhtemel bunu app.UseWatson gibi bir fonksiyonla birlikte kullanmalıyız. Önce bu seçeneği içerecek basit bir sınıfı projeye ekleyelim. İşte WatsonOptions.

using System.Net;

public class WatsonOptions
{
    public long MaxSizeForPostContent{get;set;}
}

Bu sınıf üzerinden içerik limit kontrolünü yapabilmek için maksimum boyut değerini taşıyacağız. Peki nereye? Ara katman modülümüz olan Watson sınıfına.

using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;

public class Watson
{
    private readonly RequestDelegate _nextMiddleWare;
    private readonly WatsonOptions _options;

    public Watson(RequestDelegate next, IOptions<WatsonOptions> options)
    {
        _nextMiddleWare = next;
        _options = options.Value;
    }

    public async Task Invoke(HttpContext context)
    {
        var request=context.Request;
        if(request.Method=="POST")
        {
            var contentLength=request.ContentLength;
            Console.WriteLine("[{0}]:{1}-{2}",DateTime.Now.ToLongTimeString(),request.Method,request.Path);
            
            if(contentLength>_options.MaxSizeForPostContent)
            {
                Console.ForegroundColor=ConsoleColor.Red;
                Console.WriteLine("POST size limit violation : {0} bytes\nLimit->{1}",contentLength,_options.MaxSizeForPostContent);
                Console.ForegroundColor=ConsoleColor.White;
                //TODO Something
            }
            else
            {
                Console.ForegroundColor=ConsoleColor.Green;
                Console.WriteLine("Length is OK ({0})",contentLength);
                Console.ForegroundColor=ConsoleColor.White;
            }
        }

        await _nextMiddleWare(context);                  
    }  
}

Watson, metodları rastgele tanımlanmış bir sınıf değil. Yapıcı(Constructor) metodu bir yana Task tipinden nesne döndüren ve HttpContext örneğini parametre alan Invoke isimli bir fonksiyon içermekte. Sanki ilk yazdığımız app.Use() metodunun kullandığı temsilciye(RequestDelegate) oldukça benziyor değil mi? Hatta tıpkısının aynısı diyebiliriz. Bu metod içerisinde HttpContext nesnesini kullanarak o anki talebin HTTP Post olup olmadığını kontrol ediyor ve eğer öyleyse gelen içeriğin boyutuna bakıyoruz. Şimdilik sadece ekrana bilgi yazdıran bir operasyon söz konusu. Metodun sonunda yapılan çağrı ile ilk başta gelen içeriğin çalışma zamanındaki bir sonraki ara katman modülüne devredilmesi sağlanıyor.

Sonuçta pipeline üzerinde bir metod zinciri söz konusu diyebiliriz. Ayrıca ilk başta kullandığımız app.Use() fonksiyonelliğinin sorumluluğunu başka bir tipe aldığımızı ifade edebiliriz.

Yalnız ortada küçük bir sorun var. Configure metodunda kullanılan IApplicationBuilder değişkeninin ardından UseWatson gibi bir metodu nasıl çıkartacağız? Neyse ki fii tarihinde .Net'e Extension Methods diye bir kavram eklenmiş :) Yapacağımız şey bu. Söz konusu genişletme işini WatsonExtensions isimli sınıfa şu şekilde yıkabiliriz.

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Options;

public static class WatsonExtensions
{
    public static IApplicationBuilder UseWatson(this IApplicationBuilder app,WatsonOptions options)
    {
        return app.UseMiddleware<Watson>(Options.Create(options));
    }
}

İlk parametre ile gelen app değişkeni üzerinden UseMiddlerware metodunun generic versiyonunu çağırıyoruz. Generic tipimiz Watson sınıfı. Parametre olarak da maksimum boyut özelliğini içeren options değişkenini aktarıyoruz. En dikkat çekici noktalardan birisi de UseWatson metodunun genişlettiği IApplicationBuilder tipinden bir referans döndürmesi. Resmen Fleunt bir Interface akımı söz konusu gibi ;) Yapılan bu değişikliklere göre Startup.cs içerisindeki Configure metodunda aşağıdaki gibi bir kullanım artık mümkün.

app.UseWatson(new WatsonOptions{
	MaxSizeForPostContent=1024
});

Volaaa!!! Açıkçası bu yaklaşım çok hoşuma gitti diyebilirim. Bir interface veya abstract sınıf türetimi ile plug-in mantığında bir genişletme yerine Fluent yapıyı benimseyen bir interface tipini genişleterek pipeline üzerine yeni bir middleware ekleyebildik. Bunu sıfırdan kurgulamaya çalışıp kendimizi daha da geliştirebiliriz ama şimdilik ödülümüz olan çalışma zamanı sonuçlarına bir bakalım derim. Firefox HttpRequester ile normal ve izin verilen limit üstündeki boyutlarda POST işlemleri yaptığımızda aşağıdaki ekran görüntüsündekine benzer sonuçlar alırız.

Tabii burada gelen geçen tüm mesajları boyutu ne olursa olsun işlettik. Belki de boyut kontrolünün başka bir yolu da vardır ama amacımız özel bir ara katman modülünü nasıl entegre edebileceğimizi görmekti. Hayal gücünüzü zorlayın ve nasıl ara katman ekleyebilirsiniz bir düşünün. Bu yapıyı sadece Web API tarafında değil Kestrel kullanılan tüm .Net Core projelerinde ele alabiliriz. Dilerseniz benzer örneği farklı bir senaryo ile bir MVC projesinde deneyerek bilgilerinizi pekiştirebilirsiniz. Böylece geldik bir makalemizin daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

CORS(Cross-Origin Resource Sharing)

$
0
0

Merhaba Arkadaşlar,

Geçtiğimiz günlerde Asp.Net Core tarafında SignalR kullanımını incelemeye başladım. O sırada incelediğim kaynakların birisinde UseCors isimli bir fonksiyonla karşılaştım. Uzun zamandır Cross-Site Scripting Error almamış birisi olarak .Net Core tarafında Cross-Origin Resource Sharing nasıl yapılır öğrenmem gerektiğini fark ettim. Sonunda SignalR ile ilgili araştırmalarıma bir kahve molası verip konuyu inceleyeyim dedim. Aslında W3C'un şu adresinde ve IETF(havalı isimleri ile Internet Engineering Task Force) kulübünün bu adresinde konu ile ilgili standartlara ait oldukça detaylı bilgiler mevcut.

Konuyu kısaca özetlemek için aşağıdaki grafiğin yardımcı olabileceğini düşünüyorum. Senaryoda sol taraftaki adrese erişmeye çalışan farklı kaynaklar yer alıyor. İlk senaryoda http://fabrikam.com/api/products adresine http://fabrikam.com/index.html adresinden gidilmekte. Her iki adresin de ortak özelliği aynı orjin(kök, kaynak diyelim) üzerinden sunulmaları.

Ancak izleyen senaryolarda servise doğru gelmek isteyen farklı kökler görüyoruz. Farklı bir alan adı(domain), alt alan adı(sub domain), şema(https ile gelinen) ve port. Bu durumlarda hedef adres gelen talep ile ilişkili olarak bir şüpheye düşüyor. Acaba benim iznim olmayan bir adresten mi geliniyor gibi duruma paranoyakça yaklaşıp isteğe olumlu cevap vermiyor. Bu durumlara özellikle AJAX modelli servis çağrılarının yapıldığı çözümlerde sıklıkla rastlanıyor. Dolayısıyla hedef tarafının belirli bir politikaya göre istekte bulunanlara izin vermesini sağlamamız gerekiyor. Yani bir CORS policy'nin uygulanması gerekmekte.

Gelin .Net Core tarafında CORS politikalarının nasıl uygulanabileceğini basit bir örnek ile incelemeye çalışalım. İlk olarak Illegal Cross Site Script vakasını değerlendirelim. Senaryoyu ele alırken iki uygulama yazacağız. İlki 6001 numaralı port üzerinden yayın yapan bir Web API servisi olacak. Bu servisin tüketicisi ise 5000 nolu porttan çalışacak olan boş bir Web projesi. Terminal'den aşağıdaki komutu kullanarak Contoso isimli Web API uygulamasını oluşturalım.

dotnet new webapi -o Contoso

.Net Core'un varsayılan şablonuna göre ValuesController isimli bir hizmetimiz zaten mevcut. Yenisini yazmaya bu senaryo kapsamında gerek yok. Sadece servisin yayınlanacağı adresi değiştirmemiz yeterli. Bunun için Program.cs içeriğini aşağıdaki gibi düzenleyelim.

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

Şimdi ikinci uygulamamızı oluşturabiliriz. Aşağıdaki komutu çalıştırarak devam edelim.

dotnet new web -o Customer

Oluşan web uygulamasına ait wwwroot klasörüne index.html isimli bir web sayfası ekleyelim. Bu sayfanın görevi butona basıldığı zaman http://localhost:6001/api/values adresine talepte bulunup dönen içeriği ekrana basmak. Bunu yaparken jQuery ve AJAX çağrısı gerçekleştireceğiz.

<!DOCTYPE html><meta charset="utf-8"/><html xmlns="http://www.w3.org/1999/xhtml"><head><title>Contoso Name List</title></head><body><div><h2>Contoso Names</h2><ul id="values" /></div><div>    <input type="button" value="Get" onclick="getValues();" /><p id="Contoso" /></div><script src="https://code.jquery.com/jquery-3.2.1.js"></script><script>
    function getValues() {
      var uri = 'http://localhost:6001/api/values';
      $('#values').empty();
      $.getJSON(uri)
      .done(function (data) {
        $.each(data, function (key, value) {              
          $('<li>', { text: value}).appendTo($('#values'));
        });
      });
    }</script></body></html>

getValues fonksiyonu içerisinde getJSON jQuery operasyonunu kullanarak webApi adresine talepte bulunuyor, dönüne içerikte dolaşarak herbir veriyi birer listItem olarak values isimli div altına ekliyoruz. jQuery'nin 3.2.1 versiyonunu kullanmaktayız. index.html, static bir web sayfası olduğundan web uygulamasının Startup.cs içeriğinde ufak bir değişiklik yapmamız gerekiyor.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
	if (env.IsDevelopment())
	{
		app.UseDeveloperExceptionPage();
	}
	app.UseStaticFiles();
}

Her iki uygulamada hazır. Servis ve web uygulamalarını ayrı ayrı çalıştıralım ve Get butonuna basarak servise çağrıda bulunmaya çalışalım.

İki uygulamayı da dotnet run komutlarımız ile çalıştırdıktan sonra Get ile servisten sunulan değer listesine ulaşamadığımızı görürüz.

Dikkat edileceği üzere CORS Header 'Access-Control-Allow-Origin' missing şeklinde bir uyarı mesajı alınıyor. Bir başka deyişle Contoso servisine http://localhost:5000 adresinden gelinmesi için gerekli policy belgesinin olmadığı belirtilmektedir. Bu sorunu çözmek için servis tarafındaki Startup.cs içeriğine bir kaç dokunuş gerçekleştirmemiz yeterli.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace Contoso
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

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

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseCors(bldr=>bldr.WithOrigins("http://localhost:5000"));
            app.UseMvc();
        }
    }
}

ConfigureServices metounda AddCors fonksiyonu ile Cross-Origin Sharing hizmetini çalışma zamanına ekliyoruz. Configure fonksiyonundaki UseCors çağrısı ile de Middleware tarafında ilgili policy'nin etkinleştirilmesini sağlıyoruz. Örneğimizde localhost:5000 adresi için gerekli Header bilgisinin ekleneceğini belirtmekteyiz. Şimdi senaryomuzu tekrar işletelim. Bu kez aşağıdaki görüntüde olduğu gibi değerlerin sayfaya basıldığını görebiliriz.

Bu başarılı talebin arka plandaki izlerine baktığımızda servis tarafından döndürülen cevabın içerisinde ek bir Header bilgisi olduğunu da görürüz.

Dikkat edileceği üzere Access-Control-Allow-Origin isimli header bilgisi için http://localhost:5000 değeri eklenmiştir. Benzer şekilde Request Header içerisinde de Origin bilgisi yer almaktadır.

Aslında CORS politikalarını MVC tabanlı projelerde farklı seviyelerde uygulama şansına da sahibiz. Belli bir Controller operasyonunda uygulamak istersek ilgili fonksiyona EnableCors niteliğini(attribute) belirtilen policy adıyla eklememiz yeterlidir. Benzer şekilde bu niteliği Controller seviyesinde de kullanabiliriz. Bu durumda Controller içerisindeki tüm operasyonlar için nitelik ile belirtilen CORS Policy talimatı uygulanacaktır. Action, Controller bazında uygulanabilen bu yapıyı global seviyede de kullanabiliriz. Ayrıca DisableCors niteliği ile action ve controller bazında CORS dışı bırakılma fonksiyonelliğini de sağlayabiliriz. Bu niteliklerin nasıl kullanılabildiğini araştırmanızı öneririm. (Bazı haller tarayıcıya bağlı olarak farklılıklar gösterebiliyormuş. Ben Firefox kullandım ama Chrome ve IE gibi tarayıcılarda da durumu irdelemekte yarar var)

CORS bu yazıda ele aldığımız kadar basit de değil aslında. Söz gelimi kaynak adres için gelen taleplerin belirli kriterlere göre filtrelenerek farklı kök adresler için kullanıma açılması gerekebilir. Örneğin belirli bir kökten herhangi bir Header içerecek taleplerden sadece HTTP GET, POST metodunu kullananlara izin vermek istediğinizi düşünelim. Bu durumda aşağıdaki gibi kodun yeniden düzenlenmesi gerekebilir.

app.UseCors(bldr=>bldr
   .WithOrigins("http://localhost:5000")
   .WithMethods("GET","POST")
   .AllowAnyHeader()
);

Böylece geldik bir makalemizin daha sonuna. Bu yazımızda Cross Origin Resource Sharing konusunun .Net Core WebAPI servislerinde nasıl etkinleştirilebileceğini çok basit bir örnekle incelemeye çalıştık. Sanırım SignalR çalışmalarıma geri dönebilirim. Görüşmek ümidiyle hepinize mutlu günler dilerim.

.Net Core ile ilgili diğer örnekleri GitHub adresinden çekebilirsiniz.

.Net Core Tarafında SignalR Kullanımı

$
0
0

Merhaba Arkadaşlar,

Bir süre önce araştırmaya başladığım ama araya giren diğer konular(WebSockets ve CORS-Cross Origin Resource Sharing) nedeniyle askıda kalan SignalR mevuzusuyla ilgili West-World'de haftasonu önemli ve heyecanlı gelişmeler oldu. Epey zorlandığımı itiraf etmek isterim. Bunun en büyük sebebi standart öğretilerde yer alan web tabanlı örnekler yerine her şeyi Console üzerinde uygulamaya çalışmamdı. HUB için bir sunucu, mesaj yayını için bir başka uygulama ve yayınlanan mesajları alan bir diğeri.

Alınan sayısız çalışma zamanı hatası ve uykusuz bırakan birkaç saatin ardından sonunda konuyu bir şekilde toparlamayı başardım. Bu hataları gidermeye çalışırken farkına vardığım bir çok şey de oldu. SignalR'ın çalışma yapısı haricinde sunucu ve istemci taraflarının birbirleri üzerinden fonkisyon tetiklemesi noktasında nasıl bir yol izlediklerini .Net Core cephesinden görmüş oldum. Tüm bu didinmenin ardından güzel bir uyku çektim ve ertesi gün gelecekteki kendime not bırakmak için geçtim bilgisayarımın başına. Öncelikle çalıştığım kaynaklardan yararlanarak aşağıdaki özet şekli oluşturdum.

Şekilden de görüldüğü üzere olayın ana noktasında bir HUB yer alıyor. Bunu host eden bir uygulama kendisine bağlı olan diğer uygulamaların aynı anda etkileşimde olmasına olanak tanıyor. Genellikle meydana gelen bir olay sonrası(stok hareketlerindeki değişim, oyun ağına bağlı oyunculardan birisinin yaptığı bir hamle, kanala atılan ortak bir mesaj-hey gidi MIRC vb) hub üzerine bırakılan mesaj, bağlı olan tüm istemcilere iletilmekte. İstemciler basit web tarayıcıları üzerindeki uygulamalar olabileceği gibi, mobil çözümler, terminal programları vb de olabilir. Kritik olan nokta HUB sunucusunun kendisine bağlı istemciler üzerinde fonksiyon çağırabilmesi. Tam tersi iletişim zaten hepimizin aşina olduğu bir durum. SignalR'ın bu oluşum içerisindeki veri alışverişini kolaylaştıracak nimetler sunduğunu ifade edebiliriz. Neredeyse her tipten istemci için yazılımış kütüphaneler mevcut.

Şeklin sağ tarafında SignalR Hub ile istemci arasındaki temel iletişim sırası da yer alıyor. İstemci öncelikle HUB'a bağlantı isteği gönderiyor. Bağlantı başarılı bir şekilde sağlanırsa arada kalıcı bir iletişim hattı tesis ediliyor. Sonrasında HUB ile kullanıcısı arasında bir etkileşim süregeliyor. Burada istemci HUB üzerine mesaj bırakabileceği gibi dinleme modunda da çalışabiliyor. Dinleme modu ağırlıklı olarak HUB'a mesaj bırakan bir başka istemcinin mesajını almak gibi de düşünülebilir. 

SignalR, Asp.Net tarafında uzun zamandır beri var olan bir konu olmasına rağmen, .Net Core tarafında halen Alpha sürümü olarak yer alıyor(Yazıyı okuduğunuz tarih itibariyle kontrol etmenizi öneririm. Release versiyon çıkmış olabilir) Tabii son sürüm yayınlanmadan bir gerçek hayat projesinde kullanmak pek doğru olmayacaktır.

HUB Sunucusu

Sözü fazla uzatmadan örneğimize geçelim dilerseniz. Senaryomuz standartların biraz dışında. Konuyu kavrayabilmek açısında HUB örneğini sunan bir sunucumuz, buraya abone olup mesaj yayını yapacak ve dinleyecek olan istemcilerimiz birer Console uygulaması olacak. İşe ilk olarak sunucu tarafını yazarak başlayalım(Bu arada örneği Ubuntu üzerinde Visual Studio Code ile geliştirdiğimi belirtmek isterim)

dotnet new console -o FabrikamServer

komutu ile FabrikamServer isimli bir Console uygulaması oluşturalım. SignalR kullanımı için Microsoft.AspNetCore.All ve Microsoft.AspNetCore.SignalR.1.0.0-alpha2-final paketlerine ihtiyacımız bulunuyor. Aşağıdaki terminal komutları ile bunları projemize dahil edebiliriz.

dotnet add package Microsoft.AspNetCore.All
dotnet add package Microsoft.AspNetCore.SignalR -v "1.0.0-alpha2-final"

Şu an için versiyon bilgisini belirtmemiz önemli. İşlerin yolunda gittiğinden emin olmak için belki bir restore ve build işlemi uygulamakta yarar olabilir. Uygulamada bir kaç sınıfımız var. Web çalışma ortamının ayağa kaldırılmasında kullanacağımız Startup.cs ve Hub görevini üstlenecek olan QutoeHub.cs. Evet tahmin edeceğiniz üzere oyunlardan beğendiğim bir kaç sözün yayınlanmasını sağlayacağız. Tabii online olan istemcilere.

using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;

public class QuoteHub 
    : Hub
{
    public string Send(string incomingQuote)
    {   
        Clients.All.InvokeAsync("GetQuote",incomingQuote);
        return $"[{Context.ConnectionId}]: {incomingQuote}";
    }
}

Hub sınıfından türetilmiş olan QuoteHub tipinde Send isimli bir metod yer alıyor. Bu metod o an bağlı olan istemci için üretilen GUID değeri ile birlikte parametre olarak gelen içeriği geriye döndürmekte. Ancak önemli bir görevi daha var. Bağlı olan ne kadar istemci varsa hepsindeki GetQuote operasyonunu tetiklemek. Buna göre, HUB'a bağlı olan bir istemci, Send mesajını kullanarak bir bilgi bıraktığında(ki senaryomuzda bu güzel bir oyun cümlesi), dinlemede olan ne kadar istemci varsa iletilecek. İşte basit bir broadcasting senaryosu. Startup sınıfı ile devam edelim.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
public class Startup
{
    public void ConfigureServices(
        IServiceCollection services)
    {
        services.AddSignalR();
    }

    public void Configure(
        IApplicationBuilder app, 
        IHostingEnvironment env)
    {
        app.UseSignalR(routes =>
        {
            routes.MapHub<QuoteHub>("QuoteHub");
        });
    }
}

Startup sınıfında tipik olarak route tanımlamasını yapıyor ve Middleware'e SignalR kabiliyetlerini ekliyoruz. Tahmin edileceği üzere istemciler belli bir HUB adresine doğru gelmeliler. Buradaki eşleştirme MapHub fonksiyonu ile belirtiliyor. Uygulamanın Program.cs içeriği ise şu şekilde geliştirilebilir.

using System;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;

namespace FabrikamServer
{
    class Program
    {
        static void Main(string[] args)
        {
            WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>()            
            .Build()
            .Run();
        }
    }
}

Tipik olarak Kestrel çalışma zamanının ayağa kaldırıldığını ifade edebiliriz.

Yayıncının Geliştirilmesi

Gelelim mesaj yayını yapacak olan uygulamaya. Aslında bu da bir nevi istemci gibi düşünülebilir. Tek farkı sadece Send operasyonunu çağıracak ve GetQuote ile ilgili hiçbir şey yapmayacak olması. FabrikamPostman isimli Console projesinde Microsoft.AspNetCore.SignalR.Client paketinin 1.0.0-alpha2-final sürümünün kullanılması gerekiyor(Bu Client versiyonu dikkat edin) Bu nedenle ilgili paketi hem bu hem de biraz sonra yazacağımız Console projelerine aşağıdaki komutu kullanarak eklemeliyiz.

dotnet add package Microsoft.AspNetCore.SignalR.Client -v "1.0.0-alpha2-final"

Program.cs içeriğini aşağıdaki gibi geliştirebiliriz.

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Client;

namespace FabrikamPostman
{
    class Program
    {
        static void Main(string[] args)
        {   
            HubConnection conn = new HubConnectionBuilder()
                 .WithUrl("http://localhost:5000/QuoteHub")
                 .WithConsoleLogger()
                 .Build();

            conn.StartAsync().ContinueWith(t=>{
                if(t.IsFaulted)
                    Console.WriteLine(t.Exception.GetBaseException());
                else
                    Console.WriteLine("Connected to Hub");

            }).Wait();

            conn.On<string>("GetQuote", param => {                
            });
            for(int i=0;i<10;i++)
            {   
                Random random=new Random();
                int index=random.Next(0,QuoteFabric.GetQuotes().Count);
                conn.InvokeAsync<string>("Send",QuoteFabric.GetQuotes()[index].ToString())
                .ContinueWith(t=>{
                    if(t.IsFaulted)
                        Console.WriteLine(t.Exception.GetBaseException());
                    else
                        Console.WriteLine(t.Result);
                });
                Thread.Sleep(10000);
            }

            conn.DisposeAsync().ContinueWith(t=>{
                if(t.IsFaulted)
                    Console.WriteLine(t.Exception.GetBaseException());
                else
                    Console.WriteLine("Disconnected");
            });
        }
    }
}

İlk olarak bir HubConnection nesnesi örneklenmekte. WithUrl fonkisyonuna parametre olarak geçilen adrese dikkat edelim. Az önce yazdığımız sunucunun yayın yaptığı adresteki yönlendirme bilgisine göre belirlenmiş durumda. Console penceresine log bırakacağımızı da belirtmekteyiz. İşlemleri izlemek keyifli olacak. 

StartAsync operasyonu ile bağlantının açılmasını sağlıyoruz. On metodu GetQuote için yapılan çağrıları dinlemek için ele alınmakta. Ancak az öncede belirttiğimiz gibi bu operasyonu mesaj yayını yapan programımız için kullanılmıyor. "O zaman yazmasaydın?" dediğinizi duyar gibiyim. Hemen QuoteHub üzerindeki Send fonksiyonuna dönelim o zaman. Orada tüm bağlı istemciler GetQuote çağrısı söz konusu. Burası da bir istemci olduğu için ilgili operasyonun bulunması gerekiyor. Aksi durumda çalışma zamanı hatası alırız.

10 elemanlı döngü içerisinde yapılan InvokeAsync çağırısı önemli. İlk parametre dikkat edileceği üzere QuoteHub sınıfındaki Send metodunun adı. Sonrasında ise göndereceğimiz parametre söz konusu. Burada QuoteFabric(github üzerinden yayınladığım proje kodlarını bu adrestençekebilirsiniz) sınıfı içerisinde yer alan bir kaç özlü sözden rastgele seçilen birisinin gönderildiğini ifade edebiliriz. Send metodu geriye de bir string veri döndürdüğünden ContinueWith içerisinden bu sonucu yakalamamız da mümkün(t.Result)

Bu 10 cümlelik görev tamamlandıktan sonra DisposeAsync çağrısı yapılarak bağlantının kesilmesi sağlanıyor.

Tipik İstemci

Son olarak istemci tarafını yazabiliriz. İstemci Send metodu ile bir çağrı yapmayacak. Sadece GetQuote mesajlarını dinleyecek. Console olarak oluşturacağımız projeye Microsoft.AspNetCore.SignalR.Client paketini ekledikten sonra Program.cs içeriğini aşağıdaki gibi geliştirebiliriz.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Client;

namespace FabrikamPostman
{
    class Program
    {
        static void Main(string[] args)
        { 
            HubConnection conn = new HubConnectionBuilder()
                 .WithUrl("http://localhost:5000/QuoteHub")
                 .WithConsoleLogger()
                 .Build();

            conn.StartAsync().ContinueWith(t=>{
                if(t.IsFaulted)
                    Console.WriteLine(t.Exception.GetBaseException());
                else
                    Console.WriteLine("Connected to Hub");

            }).Wait();              

            conn.On<string>("GetQuote", param => {
                Console.WriteLine(param);
            });

            Console.WriteLine("Press any key to exit.");
            Console.ReadLine();
            conn.DisposeAsync().ContinueWith(t=>{
                if(t.IsFaulted)
                    Console.WriteLine(t.Exception.GetBaseException());
                else
                    Console.WriteLine("Disconnected");
            });
        }
    }
}

Bir önceki uygulamadan pek bir farkı yok neredeyse. Yine bir HubConnection nesnesi örnekleniyor, StartAsync ile bağlantı açılıyor ve On olay metodu ile GetQuote için yapılan mesaj yayınları dinleniyor. Bir tuşa basıldıktan sonra da aradaki iletişim kopartılıyor. Tabii GetQuote operasyonuna gelecek olan mesajlar QuoteHub'daki Send metodu içerisinden yayınlanmakta(Clients.All.InvokeAsync("GetQuote",incomingQuote))

Artık bir kaç test yapabiliriz. İlk olarak HUB sunucusunu sonra yayın yapan istemciyi ve son olarak da dinleyici rolündeki programı çalıştıralım. Dinleyici rolündeki programdan bir kaç tane çalıştıraraktan da sonuçları irdeleyebiliriz.

Ekran görüntüsünden de görüldüğü üzere ben iki istemci (FabrikamSomeClient) çalıştırarak sonuçları değerlendirdim. FabrikamPostman üzerinden yayınlanan sözler, bağlı olan tüm istemcilere ulaştırıldı. Ayrıca FabrikamServer üzerindeki log izlerine bakıldığında bağlanan herbir uygulama için benzersiz Guid üretildiği de gözlemlendi. SignalR'ın WebSocket modelini baz alan eş zamanlı haberleşebilme yeteneklerini kolaylaştıran yanlarını az çok bu örnekle anlamış bulundum. Umarım sizler için de anlaşılır olmuştur. Böylece geldik bir makalemizin daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Nginx Üzerinde Web API Servisi Çalıştırmak

$
0
0

Merhaba Arkadaşlar,

Animasyon izlemek keyif aldığım hobilerden birisi. İzlediğimde bende derin izler ve duygular bırakmış uzun metrajlı bir çok anım var. Bazen filmde geçen bir söz bazen karakterin gülme krizine sokan bir davranışı ya da başına gelen dramatik bir olay. Geçenlerde bilmem kaçıncı kez izlediğim Sing filmi de benim uzun metraj anı defterim arasında yer alanlardan. Maddi sorunlar nedeniyle banka ile başı dertten kurtulamayan Buster Moon, filmin bir yerinde tam bütün ümitlerin tükendiği noktada şöyle bir cümle sarf ediyor;

"When you've reached rock bottom, there's only one way to go, and that's up!"

Türkçe metrajlı çevirisinde, "dibe indiğin zaman gidilecek tek bir yön kalır. O da, YUKARISIIII!" gibi bir şeydi yanlış hatırlamıyorsam. Benim içinde 2017 sonu ve 2018 başlangıcı bu sözde ifade edilene benzer oldu diyebilirim. Şimdi merkez stüdyolarımıza bağlanıyoruz.

Bir zamanlar üzerinde çalıştığım bir vakada, WCF ile yazılmış bir REST servisinin Authorization mekanizmasını özelleştirip, IIS üzerinden SSL ile host etmek için uğraşmaktaydım. Servisi yazmak, IIS'e atmak, IIS tarafında deneysel sertifika üretip siteyi SSL ile çağrılabilir hale getirmek, ServiceAuthorizationManager türevli sınıfı devreye alarak yetkilendirme sürecini ele almak kolaydı. Birkaç küçük ince ayarlama derken kendimi test sahasında buldum. Ne var ki bir şekilde servisin ayağa kalkması sırasında hatalar alıyordum. Sonunda olan oldu ve uğraştığım senaryodan sıkılıp bunaldığım bir noktaya geldim.

Kendi kendime "ben bunu West-World ya da Gondor üzerinde deneyeyim" diyordum. Her ikisi de Ubuntu 16.04 versiyonlarına sahip bilgisayarlardı. Kolları sıvadım ve bir Web API servisi oluşturmak üzere terminalin başına geçtim. Ne var ki ortada önemli bir sorun vardı. Linux ve IIS. Ovv yooo...Tabiki böyle bir birliktelik olamazdı. Eldeki alternatiflerse belliydi. Apache Server, Lighttpd ve Nginx. Gözüme kestirdiğimse Nginx oldu. Konu bir anda bambaşka bir yere geldi tabii. Şimdiki amacım, West-World'de kuracağım NGinX ortamı üzerinde örnek bir Asp.Net Core Web API servisini host ettirmekti.

Nginx

Nginx(Engine X olarak telafuz ediliyor) Kazakistan Almatı doğumlu bilgisayar programcısı Igor Sysoev(soyisminde sys var) tarafından 2002 yılında geliştirilmeye başlanmış ve 2004 yılında ürünleşmiş açık kaynak bir web sunucusudur. İlk olarak mail.ru için mail sunucusu olarak geliştirilmiş ama sonrasında çok daha geniş yetenekler kazanarak web siteleri için Apache'den çok daha hızlı çalışabilen bir ürün haline gelmiştir. Henüz doğrulayamadığım ama genel kabul görmüş bazı performans testlerine göre muadili olan Apache ve Lighttpd gibi ürünlere göre çok yüksek cevap süreleri ve minimum bellek tüketimi sağlamaktadır. Bu açılardan oldukça popüler olduğunu ifade edebiliriz. Yük dengeleme(Load Balancing), Sanal sunucu(Virtual Host), Otomatik indeksleme ve ters vekil sunucu(Reverse Proxy) gibi temel özellikleri vardır.

Nginx'in geliştirilme hikayesinin detaylarını okumaya devam ediyorum. Aslında C10K sorunu olarak adlandırılan bir vakaya istinaden geliştirilmiş. C10K, bir web sunucusunun eş zamanlı onbin talep üstünü kaldıramaması olarak ifade ediliyor (Şu adresten Kegel'in konu ile ilgili dokümanına ulaşabilirsiniz) Diğer yandan Nginx, ağırlıklı olarak Apache ile karşılaştırılmakta. Dikkatimi çeken en önemli fark ise Apache'nin Multi-Thread çalışırken Nginx'in talepleri karşılama noktasında Single-Thread çalışma prensibini kullanması.

Kurulum

Bu düşünceler ile birlikte her zamanki gibi çalışma odamın yolunu tuttum. Amacım basit bir Web API hizmetini(standart şablonda üretilecek olan) nginx üzerinde host etmek. Öncesinde West-World'e NGinx'i kurmam gerekiyor. Terminal'den aşağıdaki komutları kullanarak kurulum adımlarını gerçekleştirdim.

sudo apt-get install nginx
sudo service nginx start
sudo service nginx status

install komutu ile kurulum yapıldıktan sonra start ile nginx servisini başlatıyoruz. status ile de güncel durumunu görüyoruz. Yeşil renkteki active(running) yazısını görmek güzel.

Yönlendirme Ayarları

Nginx kurulduktan sonra Reverse Proxy Server olarak çalışması için bir ayar yapmam gerekiyor. Bu sayede Nginx sunucusuna gelen talepler Web API'nin ayağa kalktığı Kestrel sunucusunun ilgili adresine yönlendirilecekler. Pek tabii tam tersi istikamette söz konusu. Aslında istemciler hiçbir şeyin farkında olmayacaklar. Web API'ye tarayıcıdan yapılan HTTP çağrıları aslında Nginx sunucusu tarafından karşılanıp arka taraftaki asıl Kestrel çalışma zamanına akıtılacak. Bunun üzerine /etc/nginx/sites-available/default dosyasını gedit ile açıp içeriğinde aşağıdaki düzenlemeyi yaptım.

server 
{
   listen 81; 
   location / 
   {      
      proxy_pass http://localhost:5000;
      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection keep-alive;
      proxy_set_header Host $host;
      proxy_cache_bypass $http_upgrade;
   }
}

Normalde localhost:80 şeklinde 80 portunun dinlenmesi söz konusu ama makinede 80 portunu kullanan Apache sunucusunu bozmak istemedim doğruyu söylemek gerekirse. Bu yüzden deneme olması açısından 81 nolu portu ele alıyorum. proxy_pass bildirimine göre http://localhost:81 adresine gelecek olan istekler http://localhost:5000 'e yönlendirilecek ki bu da Web API uygulamasının varsayılan olarak çalıştığı adres(biliyorsunuz bu adresleri senaryoya göre değiştirebiliriz)

Yapılan değişiklikleri devreye alabilmek için terminalden bir kaç işlem daha yapmak gerekiyor.

sudo nginx -t
sudo nginx -s reload

İlk komut ile konfigurasyon için yapılan değişiklikler test edilip bir sorun olup olmadığına bakılıyor. reload komutu da yeni değişikliklerin çalışma zamanına yeniden yüklenmesi için kullanılmakta. 

Service Definition Dosyasının Oluşturulması

Şimdi biraz düşünelim. Nginx platform bağımsız bir web sunucusu. Yazacağım Asp.Net Web API uygulaması ise normal şartlarda kendi web sunucusunu(Kestrel) kullanıyor. Her ikisi de farklı Process'ler anlamına gelebilir. Dolayısıyla Nginx'in yazılacak WebAPI servisi özelinde ilgili Kestrel Process'ini ayağa kaldıracağını bilmesi gerekiyor. IIS'in Worker Process çalışma modeline benzer bir yaklaşım olarak düşünelim. Bunun için service uzantılı bir dosya yazmak lazım. Senaryomda bunu kestrel-baseapi.service şeklinde isimlendirdim (Dosyanın oluşturulduğu yer önemli. etc altındaki systemd altındaki system atlında olsun lütfen)

sudo gedit /etc/systemd/system/kestrel-baseapi.service

Dosya içeriği ise şu şekilde

[Unit]
Description=BaseWebAPI(Standard Asp.Net Web API 2.0 project) on NGinx

[Service]
WorkingDirectory=/var/www/core/BaseWebAPI
ExecStart=/usr/bin/dotnet/ /var/www/core/BaseWebAPI/BaseWebAPI.dll
Restart=always
RestartSec=30
SyslogIdentifier=dotnet-BaseWebAPI
User=www-data
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false

[Install]
WantedBy=multi-user.target

Dosya içerisinde neler var şöyle bir bakmakta yarar var. Unit, Service ve Install isimli üç parçadan oluşan bir içerik söz konusu. Eminim çok daha fazla detay yazılabiliyordur ama Nginx kaynaklarından öğrendiğim kadarıyla bu içerik yeterli. En önemli bölüm service kısmı. WorkingDirectory ile WebAPI projesinin olduğu yer işaret ediliyor(ki henüz oluşturmadım) ExecStart komutu iki parametre alıyor. İlki dotnet programının yerini işaret etmekte. İkinci parametre ise Web API uygulamasının publish edilen dll dosyasını. Bir başka deyişle Nginx'in talep sonrası dotnet run BaseWebAPI işlemini uygulaması sağlanıyor. Restart ve Restartsec özelliklerine atanan değerler, ilgili web uygulamasının başı belaya girerse 30 saniye sonra tekrardan restart edilmesini belirtmekte. SyslogIdentifier ile de uygulamanın Nginx tarafında izlenirken kullanılacak olan takma ad olarak düşünülmeli. Diğer Environment değerleri ne anlama geliyor henüz bilmiyorum ama öğrenmek için epey zamanım var.

Web API Hizmetinin Yazılması

Standart web api şablonundan bir proje oluşturup bunu uygun klasöre publish etmem gerekiyor. Komut satırından her zaman yaptığım gibi projeyi oluşturuyorum.

dotnet new webapi -o BaseWebAPI

Bu adımdan sonra startup.cs dosyasında bir değişiklik yapılması öneriliyor. Bunun sebebi Reverse Proxy'ye gelen taleplerin NGinx'in istediği Header türlerine dönüştürülmesi gerekliliği. 

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace BaseWebAPI
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

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

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseForwardedHeaders(new ForwardedHeadersOptions
            {
                ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
            });
            app.UseAuthentication();
            app.UseMvc();
        }
    }
}

Söz konusu dönüşümler için Middleware katmanına müdahale edilmesi lazım. UseForwardedHeaders fonksiyonu bu noktada devreye girmekte. Bu işlemin ardından Publish ile dll üretimini yapmam yeterli. Publish adresi olarak service dosyasında belirttiğim klasörü kullanmayı tercih ediyorum.

sudo dotnet publish -o /var/www/core/BaseWebAPI

Toparlayabilirim

kestrel-baseapi.service ve Web API uygulaması hazır olduğuna göre systemctl(linux tabanlı sistemler için kullanılan servis yöneticisidir. Aslında systemd'nin organizasyonundaki çeşitli servislerin yönetilmesinde kullanılır. Buradaki senaryoda Nginx hizmetleri mevzubahistir) ile service dosyasını çalıştırabilir ve son durumu izleyebilirim. Terminalin başına geçiyorum ve aşağıdaki komutları sırasıyla çalıştırıyorum.

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

İlk komut ile yazılan service dosyası devreye alınmakta(disable ile devre dışı kalacağını da belirtelim) İkinci komut ile servis başlatılmakta ve son komutla da anlık durum hakkında bilgi alınabilmekte. Aslında hepsi bu kadar. Tarayıcımdan http://localhost:81/api/values şeklinde talepte bulunduğumda aşağıdaki gibi Web API servisinin çalıştırıldığını görüyorum. İşte bir mutluluk anı daha.

Hatta kestrel-baseapi.service hizmetini pasif hale getirip aynı talebi yaptığımda Nginx'in bana o güzelim 502 hata sayfasını gösterdiğini de görüyorum. Bu gerçekten de istenildiği gibi proxy'nin çalıştığının ispatı aslında.

systemctl stop kestrel-baseapi.service

sonrası durum

Benim için araştırması, öğrenmesi ve yazılması keyifli bir makalenin daha sonuna gelmiş bulunuyoruz. Bu yazıda bir Asp.Net Core Web API uygulamasının Ubuntu'da yüklü Nginx web sunucusu üzerinden nasıl host edilebileceğini incelemeye çalıştım. Microsoft ve .Net ürünlerine aşina olanlar için IIS yerine NGinx'i koyduk diyebiliriz de. Tabii işin çok daha fazla detayı olduğunu biliyoruz. Örneğin Load Balancing mevzusu, SSL tabanlı iletişimin kurulması, Authentication mekanizmaları vb konular var. Bunları da incelemeye çalışacağım. Ancak konuya küçük bir grizgah yaptığımızı da ifade edebilirim sanıyorum ki. Bu arada özellikle Microservice mimarisinin yüksek düzey mimari görünüme sahip basit bir örnek üzerinden incelemek isterseniz Nginx'in şu adresteki yazısınışiddetle tavsiye ederim. Renkli kalemlerinizi de hazır edin. Yazıyı okurken mimari çizimleri siz de çizmeye çalışın ve bilginizi pekiştirin derim.

Örnekte Uber benzeri bir taksi çağırma ürününün geliştirilmesi konu alınmış. Önce mimari bildiğimiz Monolithic yaklaşım üzerine kurgulanıyor. Her şey güllük gülistanlık giderken iş birimlerinden gelen yeni talepler, devreye alınan parçlar ve kalabalıklaşan kodlar sebebiyle sistem büyüdükçe büyümeye başlıyor. Performans düşüyor, yönetim ve ölçeklenebilirlik zorlaşıyor, bakım maliyetleri artıyor. Sonrasında aynı senaryonun micoservice mimari yaklaşımı ile nasıl ele alınabileceği masaya yatırılıyor. Her iki yaklaşımın artı ve eksilerini görebilmek açısından oldukça güzel ve doyurucu bir yazı olduğunu ifade etmek isterim.

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

AWS Lambda Üzerinde .Net Core Koşturmak

$
0
0

Merhaba Arkadaşlar,

Çok yeni bir dünyanın içerisindeyiz uzun zamandır. Cloud Computing ile başladı. C#'ın Linux, MacOSX üzerinde çalıştığına şahit olurken, React Native'ın dünyayı sarsan yükselişine tanık olduk. Oysa ki daha bir süre öncesine kadar Scrum metodolojilerine alışmaya çalışıyor, TFS'e nasıl plug-in yazarıza bakıyorduk. Üniversite yıllarımızda internet bağlantısı bile olmayan bilgisayarlarda yazdığımız faktöryel hesaplama fonksiyonlarını, doğrudan sahibi olmadığımız Quantum bilgisayarlara yaptırmak için Serverless sistemlerden yararlanabileceğimiz bir dünya söz konusu artık. Şirketin hantallaşan iş alanlarını birer microservice haline getirip docker üzerinden host ettiğimiz gezegenler var artık. Çok hızlı dediğimiz Apache sunucularının yerini alan NGinx'e bakarken IIS'i unutup gidiyoruz belki de. Gelişiyoruz, değişiyoruz...Ve bu ikisini sürekli yapıyoruz. Adapte olmak zorundayız.

Konumuz Serverless, AWS Lambda ve .Net Core.

Günümüz bulut sistemleri göz önüne alındığında Microsoft Azure, Amazon Web Services ve Google Cloud Platform ilk aklımıza gelen ürünler oluyorlar sanıyorum ki. Neredeyse tamamının serverless olarak bildiğimiz yetenekleri de bulunuyor ki son yılın belki de en moda kavramlarından birisi bu. Kısaca Backend As A Services(BaaS) ya da Function As A Services(FaaS) şeklinde anılan Serverless teknolojisini halen daha anlamaya çalışıyorum. İşin aslı bu yeni yaklaşımdaki amaç, elimizdeki iş fonksiyonelliklerini sunucuların bakım, ölçekleme gibi ihtiyaçlarını düşünmeden sürekli dağıtım çarkının içerisine kolayca dahil edebilmek, hizmette oldukları süre boyunca parasını ödemek, böylece maliyetlerimizi mümkün olduğunca azaltmak.

Araştırmalarım şiddetini arttırınca gecenin bu saatlerinde internetten okuduğum bir yazıdaki şekli de aşağıdaki gibi renklendirmeye çalıştım. Şekil bir Web API uygulamasını AWS üzerine aldığımızda bilinen kullanımıyla yeni yaklaşım arasında nasıl bir fark oluştuğunu betimlemekte. Kullanıcı talepleri önce Amazon Web Service üzerindeki API Gateway'e geliyor. Talepler, Lambda üzerinden geçerek .Net Core'un çalışma zamanına inip oradan ilgili fonksiyonun işletilmesi, üretilen cevabın da geriye döndürülmesi işlemleri gerçekleşiyor. Özetle tanıdık olduğum senaryolardaki IIS ve NGinX gibi talebi karşılayan uygulamaların yerini AWS alıyor diyebiliriz.

AWS Lambda uzun zamandır ilgimi çeken bir üründü. Nitekim çok geniş bir uygulama desteği bulunuyor. Node Js, Python, Go, Ruby ve tabii .Net Core(Tek üzücü olan şey konuya hazırlandığım 2017 Aralık ayı itibariyle AWS'nin .Net Core 1.0.4 SDK'sını desteklemesiydi) Önümüzdeki aylar için yapılacaklar listeme eklediğim ödevlerden birisi de .Net Core ile yazdığım bir uygulamayı Lambda üzerinden yayınlamaktı.

Temelde AWS üzerinde 3 modelde uygulama geliştirebiliriz. Plain Lambda Function, Serverless Application ve Asp.Net Core App as Serverless Application(Çizim buna ait) Plain Lambda Function modeli ile bir C# sınıfının fonksiyonlarını AWS üzerinden tetiklenebilir hale getiriyoruz. İkinci modelde, yazılan uygulamanın API Gateway arkasında çalışacak şekilde CloudFormation(YAML veya JSON formatında belli bir şablon kuralına göre taşımaya ait bilgileri yazdığımız dil olarak düşünülebilir) kullanılarak taşınması söz konusu. Son modelde ise Asp.Net Core uygulamasının tek bir Lambda fonksiyonu haline getirilerek AWS üzerine taşınması söz konusu. Bu durumda API Gateway bir Proxy görevini üstleniyor diyebiliriz. Benim bu yazıdaki amacım ise bir Handler sınıfı ve içerisindeki fonksiyonları Lambda üzerine taşımak.

Serverless yaklaşımı ile ilgili olarak iki yakın arkadaşımın harika birer yazısı var. Deniz İrgin'in "Sunucusuz Bir Dünya Mümkün mü?" yazısına bu adresten, Arda Çetinkaya'nın da "Nedir bu Serverless?" isimli makalesine şu adresten ulaşabilirsiniz.

Pek tabii her şeyin başında AWS'de bir hesap açmak gerekiyor. Bu hesap açma işlemi sırasında sizden bir de kredi kartı istenecek(Ne yazık ki) Free Plan'ı seçmeniz benimki gibi demo uygulamalarınız için yeterli olacaktır. Kritik noktalardan birisi AWS hesabını açtıktan sonra Identity and Access Management kısmından bir kullanıcı oluşturmamızın gerekliliği. Bu kullanıcıyı çeşitli yetkiler ile donattıktan sonra(Genelde Administrator olarak) bir Application ID ve Secret Key değeri üretilecek. Bu değeleri serverless(biraz sonra değineceğim) çatısına aşağıdaki komutlar ile bildirmek gerekiyor ki taşıma(Deployment) işlemleri sırasında AWS tarafındaki authenticate adımları başarılı sonuçlansın.

serverless config credentials --provider aws --key Key_Değeri_Gelecek --secret Secret_Key_Değeri_Gelecek

Serverless, Node.js ile yazılmış olan bir CLI(Command Language Interface) aracı. Bu aracı kullanarak AWS Lambda tarafında uygulama geliştirme ve taşıma gibi çeşitli operasyonları gerçekleştirebiliriz. Bu benim için oldukça ideal bir yaklaşım. Nitekim West-World üzerinde Visual Studio bulunmuyor(Bu sene sadece Visual Studio Code kullanacağım) Dolayısıyla AWS Lambda uygulamaları için gerekli hazır proje şablonlarını yükleyebileceğim bir ortam yok. Ama CLI üzerinden serverless çatısını kullanarak gerekli geliştirmeleri pekala yapabilirim. İlk etapta Nodejs'in son sürümünün yüklü olması gerektiğini söylemeliyim. Bu yüzden aşağıdaki komutlar ile Nodejs'i sisteme yüklemek gerekiyor. 

curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -
sudo apt-get install -y nodejs

Yüklemenin ardından aşağıdaki komut ile serverless çatısını sisteme kurabiliriz.

sudo npm install serverless -g

Eğer her şey yolunda giderse aşağıdaki komutların sonucu olarak versiyon numarasını ve create komutu ile kullanabileceğimiz şablonları görebilmemiz gerekir.

serverless --version
serverless create --help

Serverless çatısının başarılı bir şekilde yüklenmesinin ardından ben, hellolambda isimli bir klasör oluşturdum ve sonrasında içerisinde aşağıdaki komutu çalıştırarak Lambda dünyasına "Hello World" dedim.

serverless create --template aws-csharp --name bssdemo

Ekran görütüsünden de görüleceği üzere C# için bir şablon otomatik olarak oluşturuldu. Buradaki Handler.cs, serverless.yml gibi içerikler hiç bozulmadan AWS'deki hesabımız ile ilişkilendirilip kullanılabilirler. Handler sınıfı içerisinde Hello isimli bir metod yer almakta. Bu metod Lambda tarafındaki fonksiyon olarak da düşünülebilir. Kabaca API Gateway'e gelecek olan talep sonrası işletilecek fonksiyon olduğunu söyleyebiliriz. Peki "Hello" isimli fonksiyonu sistem nereden bilecek? İşte serverless.yml içerisindeki aşağıdaki kısım burada devreye giriyor.

functions:
  hello:
    handler: CsharpHandlers::AwsDotnetCsharp.Handler::Hello

Tahmin edeceğiniz üzere Handler sınıfının kendisinin de içerisindeki fonksiyonun da adını değiştirebilir hatta n sayıda fonksiyonu sisteme dahil edebiliriz. Birden fazla handler'da burada söz konusu olabilir. İşin sırrı YAML içeriğinde gizli(Biraz sonra değiştireceğiz) 

Tam build işlemlerine başlamıştım ki bir hatayla karşılaştım. global.json dosyasındaki SDK versiyonu 1.0.4ü gösteriyordu. West-World ise .Net Core'un 2.0 versiyonunu kullanıyordu. Dolayısıyla AWS'nin şu an desteklediği .Net Core 1.0.4 SDK'sı makinede yüklü değildi. Hemen bu versiyonu yüklemeye karar verdim. Ama endişelerim de vardı. Ya 2.0.0 ortamı bozulursa?!

sudo apt-get install dotnet-dev-1.0.4

Bu komutla sisteme .Net Core 1.0.4 ortamını da yüklemiş oldum. Sonrasında paketi yüklemeyi tekrar denedim. Bulunduğum klasör itibariyle dotnet aracı .Net Core 1.0.4'ı dikkate almaya başlamıştı(Bu arada build ve restore işlemlerinde .Net 1.0'ın 2.0'a göre acayip yavaş olduğunu belirtmek isterim) 

Derken ilk deploy işlemi sırasında "ServerlessError: The security token included in the request is invalid." şeklinde bir hata aldım. Sorun AWS hesabıma ait Credential bilgilerinin makinem için ayarlanmamasından kaynaklanıyormuş(Thanks a lot Stackoverflow) Bunun üzerine önce aşağıdaki komutlarla gerekli kayıt işlemlerini gerçekleştirdim(Siyah ile boyalı kısımları söylemicim)

export AWS_ACCESS_KEY_ID=application_id gelecek
export AWS_SECRET_ACCESS_KEY=Secret_key gelcek
serverless deploy

Sonrasında tekrardan build ve deploy işlemlerini gerçekleştirdim.

sh build.sh
serverless deploy -v

Bir yerlere varıyor gibiydim. Küçük bir deneme yaptım. serverless çatısının invoke fonksiyonunu kullanarak şablon ile birlikte gelen hello operasyonunu çağırmayı denedim. Aşağıdaki gibi.

serverless invoke -f hello -l

Sanki uygulama Lambda üzerinden çalıştırılmış gibiydi. Bu tabii standart şablon uygulaması. Değiştirmekte yarar var. Öncelikle HTTP taleplerine cevap vermesi için Amazon.Lambda. APIGatewayeEvents paketinin çözüme dahil edilmesi gerekiyor. Aşağıdaki komut ile bu işlemi yapabiliriz.

dotnet add package Amazon.Lambda.APIGatewayEvents -v:"1.1.0"

Sonrasında Handler.cs dosyasının hem adını hem de içeriğini aşağıdaki gibi değiştirdim.

using Amazon.Lambda.Core;
using Amazon.Lambda.APIGatewayEvents;
using System;
using System.Net;
using System.Collections.Generic;

[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.Json.JsonSerializer))]
namespace AwsDotnetCsharp
{
    public class SampleHandler
    {
        public APIGatewayProxyResponse GetGreetingsMessage(APIGatewayProxyRequest request, ILambdaContext context)
        {
            var response = new APIGatewayProxyResponse
            {
                StatusCode = (int)HttpStatusCode.OK,
                Body = "{ \"Motto \": \"Merhaba ahbap. Nasıl gidiyor bakalım? :)\" }",
                Headers = new Dictionary<string, string> { { "Content-Type", "application/json" } }
            };

            return response;
        }

        public APIGatewayProxyResponse GetLocalWeatherCondition(APIGatewayProxyRequest request, ILambdaContext context)
        {
            var response = new APIGatewayProxyResponse
            {
                StatusCode = (int)HttpStatusCode.OK,
                Body = "{ \"Weather \": \"Sıcaklık 29 Derece. Hava güneşli\" }",
                Headers = new Dictionary<string, string> { { "Content-Type", "application/json" } }
            };

            return response;
        }
    }
}

SampleHandler sınıfı içerisinde iki fonksiyon yer almakta. Yaptıkları çok önemsiz. Ancak parametreleri ve dönüş tiplerine dikkat etmek lazım. Her ikisi de HTTP Get taleplerine cevap verip JSON formatında içerik döndürmekteler. Tabii eklenen bu iki fonksiyon için dağıtım kanalına da bilgi vermemiz gerekiyor. Bunun için de yml içeriğini aşağıdaki gibi düzenledim (service: bssdemo aslında benim AWS üzerinden oluşturduğum bucket'a verdiğim ad. Geliştireceğimiz .Net uygulamasını bu hizmetin olduğu yerde konuşlandıracağız)

service: bssdemo

provider:
  name: aws
  runtime: dotnetcore1.0

package:
  artifact: bin/release/netcoreapp1.0/deploy-package.zip

functions:
  greetings:
    handler: CsharpHandlers::AwsDotnetCsharp.SampleHandler::GetGreetingsMessage

    events:
      - http:
          path: greetings/hi
          method: get

  utility:
    handler: CsharpHandlers::AwsDotnetCsharp.SampleHandler::GetLocalWeatherCondition

    events:
      - http:
          path: utility/weather
          method: get

(yml içeriğindeki girintiler oldukça önemlidir. Ben weather için yazdığım path ifadesini http ile aynı hizaya koyduğumda, serverless aracı ilgili path sekmesini bulamadığını söyledi)

sevice, provider, package ve functions. AWS özellikle bu kısımlara bakacak. provider tarafında aws ve .Net Core 1.0 kullanılacağı belirtiliyor. package tarafında build işlemi sonrası oluşan artifact işaret edilmekte. functions kısmında hangi operasyonları sunacaksak onlara ait bilgiler bulunuyor. handler'lar tip ve metod adlarını ifade ederken http sonrası gelen bilgiler de URL'in şekillenmesi için. get bildirimi tahmin edeceğiniz üzere talebin HTTP Get olacağını belirtmekte.

Kodun düzenlenmesini tamamladıktan sonra tekrardan build ve deploy işlemlerini gerçekleştirmek lazım. Aslında n tane fonksiyona yeni bir fonksiyon eklediysek sadece bu fonksiyonu deploy edebiliriz de(Nasıl olabilir bir araştırın bakalım)

Artık API Gateway'in tetikleyeceği iki Lambda fonksiyonu söz konusu. Doğruyu söylemek gerekirse Postman aracı ile denemeleri yaptığımda gördüğüm sonuçlar beni mutlu etti.

İlk önce greetings/hi adresine HTTP Get talebi gönderdim.

Ardından utility/weather için bir talep daha.

Sonuçlar tatmin ediciydi. Daha önceden yazdığımız bir çok Web API'yi buraya entegre edebiliriz diye düşünüyorum. Pek tabii fonksiyonlar tamamen deneme amaçlı geliştirilmiş durumdalar. Siz içeriklerini istediğiniz gibi genişletebilirsiniz. AWS dünyası oldukça kapsamlı. Henüz MSDN rahatlığını bulabilmiş değilim dökümantasyonlarında. Bazen kayboluyorum ama ilgi çekici olduğunu da ifade edebilirim. Kavramlar yeni yeni oturmaya başladılar. Codefiction gibi web sitenizi AWS üzerine alabilirisiniz ya da şirketinizin elektronik ticaret alt yapısına ait fonksiyonellikleri burada barındırabilirsiniz. Bunları bir düşünün :) Böylece geldik bir makalemizin daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Sadece Tarayıcı Kullanarak Web API Servisini Google Cloud Platform Üzerinde Yayınlamak

$
0
0

Merhaba Arkadaşlar,

Sizi Cumartesi gecesi çalışma odama davet etsem...Olmaz mı? Pekiiii...Sadece 15 dakika içerisinde standart bir .Net Core Web API hizmetini Google Cloud Platform üzerine taşıyabileceğinizi söylesem. İlginizi çekmedi mi hala...Pekiiii...Tüm bunları sadece tarayıcı(Chrome, IE, Firefox, Opera, elde ne varsa) ile yapabileceğinizi söylesem :) Sanırım şimdi dikkatinizi çekmiş olmalı. Bu gece farklı bir çalışma denedim. Her Cumartesi olduğu gibi bu Cumartesi gecesi de konsere gitmek yerine West-World üzerinde dolaşmaya karar verdim. Hafta içinde yaptığım denemelerde Google Cloud Platform üzerinde çeşitli .net core uygulamalarını nasıl yayınlayabileceğimi incelemeye çalışmıştım. Google tarafında da işler inanılmaz derecede güzeldi. Derken yaptıklarımı yazmak yerine şöyle eş zamanlı bir video kaydı halinde tutsam daha iyi olmaz mı diye düşündüm. Windows'taki Camtasia Studio'yu aramadım desem yeridir ama Ubuntu tarafındaki OBS'de işimi gördü sayılır. Sonuç olarak hataları ve özellikle de görünmeyen fare imleci ile birlikte Youtube kanalıma yükleyebileceğim keyifli bir çalışma ortaya çıktı. Konuşma içermeyen, piyano tınıları eşliğinde süregelen sakin bir çalışma. Umarım sizler için faydalı olur.

Not: Talep sayısının artma ve Free Trial için verilen paranın bitmesi ihtimaline karşın servis şu anda hizmet dışıdır :) Bilginiz olsun.

Peki ne yaptık?

  • Chrome ortamını terk etmeden Google Cloud Platform üzerinde proje oluşturduk.
  • Bu proje üzerinde Cloud Shell'i kullanarak basit bir .Net Core Web API uygulması açtık.
  • Uygulama kodlarını Code Editor(Beta sürümünde) ile düzenledik.
  • Bir publish işlemi yapıp app.yaml dosyası ile projeyi dağıtıma hazırlandık.
  • gCloud aracını kullanaraktan deploy işlemini
  • Son aşamada yayınlana servis adresinne giderek sonuçları gördük.

Komutlar

Çalışma sırasında kullandığım komutları ise şu şekilde özetleyebilirim.

  • dotnet --version (dotnet sürümünü öğrendik ki 2.1.3 olmuştu. Restore işleminde biraz sorun oluşturdu)
  • dotnet new webapi -o PlanetAPI (webapi şablon projesini oluşturduk)
  • dotnet publish -c Release (uygulamayı publish etmek için kullandık)
  • gcloud app deploy --version v1 (uygulamamızın App Engine' taşınmasını sağladık. Bunu publish edilen paketin olduğu yerde çalıştırdık.)

Google Cloud Platform tarafı epey hoş. AWS'den sonra burada da farklı bir deneyim yaşadığımı ifade edebilirim. İlerleyen zamanlarda GCP tarafı ile ilgili bir şeyler de yazmaya çalışacağım. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.


Bir Ruby Uygulamasını Google Cloud Platform Üzerine Taşımak

$
0
0

Merhaba Arkadaşlar,

"Futbol basit bir oyundur. 22 kişi 90 dakika boyunca bir topun peşinde koşar ve sonunda her zaman Almanlar kazanır." demiş bizim de ne yazık ki yakından tanıdığımız Gary Lineker. Konumuzla ne alakası var derseniz. Az sonra onun bu sözünü buluta alacağız.

Ben bu bulut platformlarını çok tuttum. Gerek Microsoft Azure, gerek Amazon Web Services, gerek Google Cloud Platform...Hepsi çok çekici duruyor. Kurcaladığım ve üzerinde çalıştığım örneklerle, özellikle West-World dünyasında yazılmış bir programın ilgili platform üzerinde konuşlandırılması ve yürütülmesini öğrenmeye gayret ediyorum. Ağırlıklı olarak REST(Representational State Transfer) tipinden servis uygulamalarını taşımaya çalışıyorum ki bu sayede bir mikroservis bulut alanında nasıl kullanılıyor öğreneyim.

Bu seferki hedefimse rastgele özlü söz veren Ruby programlama dili ile yazılmış bir REST servisinin Google Cloud Platform'a taşınarak App Engine Flexible ortamında ayağa kaldırılması. İşlemlerime başlamadan önce Google Cloud Platform üzerinde geçerli bir ödeme seçeneğinin olması gerekiyor(AWS'de de benzer şekilde servislerden yararlanabilmek için geçerli bir kredi kartı bilgisi isteniyordu) Bir ablam olduğu için çok şanslıyım. Sağolsun bu proje için sanal kredi kartını kullanmama izin verdi. Gelin adım adım ilerleyerek ruby servisimizi App Engine Flexible ortamından çalıştıralım.

Google Cloud Tools Olmazsa Olmaz

West-World bildiğiniz üzere Ubuntu 16-04 64bit sürümünü kullanıyor. Bu nedenle sıradaki Google Cloud Tools kurulum işlemi Debian/Ubuntu sistemleri için geçerli. Elbette Windows, MacOSX, RedHat/Centos gibi sistemler için de bu araçtan yararlanabiliriz(Şu adresten kendi sisteminiz için uygun olan sürümü indirebilirsiniz) Google Cloud Tools taşıma işlemi sırasında kullanacağımız araç olduğu için yüklememiz gerekiyor. Aşağıdaki terminal komutlarını kullanarak kurulumu yapıyorum.

export CLOUD_SDK_REPO="cloud-sdk-$(lsb_release -c -s)"
echo "deb http://packages.cloud.google.com/apt $CLOUD_SDK_REPO main" | sudo tee -a /etc/apt/sources.list.d/google-cloud-sdk.list
curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
sudo apt-get update && sudo apt-get install google-cloud-sdk
sudo apt-get install google-cloud-sdk-app-engine-java
gcloud init

Son adımda Google login sayfasına yönlendirildim ve hangi projeyi hangi Google Compute Engine bölgesinde kullanmak istediğime dair iki soruyla karşılaştım. Ben YourFunnyQuote isimli projemi ve uğurlu rakamım 13 ile australia-southeast1-b bölgesini tercih ettim.

ve login sonrasında da aşağıdaki ekranla karşılaştım. 

Bir başka deyişle West-World üzerinden gcloud'u kullanabilmek için kimliğim doğrulanmıştı.

 Diğer bölgeler hakkında görsel bilgiye ihtiyaç duyarsanız şu adrese uğramanızı tavsiye ederim.

Kurulumun sorunsuz tamamlanıp tamamlanmadığından emin olmak için de şu komutu çalıştırdım.

gcloud --help

Bu işlemler sırasında seçtiğimiz proje önemli. Nitekim taşıma işlemi sonrasında kodlarımız bu proje ile ilişkiliendirimiş Instance'a taşınacak.

Ruby Uygulamasının Geliştirilmesi

gcloud aracını yükledikten sonra basit bir Ruby projesi geliştirmeye karar verdim. Ne var ki bu ana kadar West-World üzerinde hiçbir ruby projesi geliştirmemiştim. West-World bu değerli Ruby mücehverinin ne olduğunu bilmiyordu. O yüzden ona kurulması lazımdı.

sudo apt-get install ruby-full

ile tam sürümü yükledim. Kurulum sonrası

ruby -v

terminal komutu ile de yüklenen versiyondan emin oldum.

Örnek olarak REST tipinden bir servis geliştirmeyi planlıyordum. İşimi kolaylaştıracak paket ise Sinatra'ydı(Ruby on Rails çatısı da tercih edilebilir tabii ki) Ancak Ruby ile yeni tanışmış olan West-World büyük üstad Sinatra'dan da bihaberdi. Ona "I did it my way" sözlerini fısıldayarak usta sanatçıyı anımsatmaya çalıştım dersem deli olduğumu düşüneceksiniz. Bende bu nedenle 

sudo gem install sinatra

ile sisteme Sinatra'yı yüklemeyi uygun gördüm.

Ayrıca serüvenim sırasında Ruby bağımlılıklarının yüklenmesini sağlamak için ruby-bundle aracına da ihtiyacım vardı. Nitekim taşıma işlemi sırasında Google Cloud Platform, Gemfile.lock dosyasının içeriğine bakarak ilgili bağımlılıkların ortama kurulumunu gerçekleştirecekti. Onun standartları buydu.

sudo apt-get install ruby-bundler

Nihayet ruby kodlarını yazmaya başlayabilirdim. Paslanmış olan ruby bilgime aldırmadan Visual Studio Code'u açtım ve uygun bir klasör içerisinde önce app.rb dosyasını ardından özellikle Google için önemli olan app.yaml ve Gemfile içeriklerini oluşturdum.

require "sinatra"
require "json"

set :port, 8080

get "/quotes/random" do
    quotes=Array.new
    quotes<<Quote.new(122548,"Michael Jordan","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.")
    quotes<<Quote.new(325440,"Vince Lombardi","We didn't lose the game; we just ran out of time")
    quotes<<Quote.new(150094,"Randy Pausch","We cannot change the cards we are dealt, just how we play the game")
    quotes<<Quote.new(167008,"Johan Cruyff","Football is a game of mistakes. Whoever makes the fewest mistakes wins.")
    quotes<<Quote.new(650922,"Gary Lineker","Football is a simple game. Twenty-two men chase a ball for 90 minutes and at the end, the Germans always win.")
    quotes<<Quote.new(682356,"Paul Pierce","The game isn't over till the clock says zero.")
    quotes<<Quote.new(156480,"Jose Mourinho","Football is a game about feelings and intelligence.")
    quotes<<Quote.new(777592,"LeBron James","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.")
    quotes<<Quote.new(283941,"Roman Abramovich","I'm getting excited before every single game. The trophy at the end is less important than the process itself.")
    quotes<<Quote.new(185674,"Shaquille O'Neal","I'm tired of hearing about money, money, money, money, money. I just want to play the game, drink Pepsi, wear Reebok.")

    content_type 'application/json'
    index=rand(quotes.length-1)
    quote=quotes[index]
    quote.to_json
end

class Quote
    attr_accessor:id
    attr_accessor:owner
    attr_accessor:text
     
    def initialize(id,owner,text)
        @id=id
        @owner=owner
        @text=text
    end
    def to_json(*a)
        {
            "json_class" => self.class.name,
            "data"       => {"id" => @id, "owner" => @owner, "text" => @text}
        }.to_json(*a)
    end
    def self.json_create(object) 
        new(object["data"]["id"], object["data"]["owner"],object["data"]["text"])
    end
end

Kod oldukça basit. Quote sınıfına ait nesne örneklerinden oluşan bir dizi var. Quote sınıfı JSON serileştirme için gerekli fonkisyonellikleri de barındırıyor. sinatra çatısını kullanan uygulamanın HTTP Get ile erişilebilen bir metodu var. Buna göre localhost:8080 adresine gelip quotes/random yoluna gittiğimizde, üretilen rastgele sayıya göre diziden bir özlü sözün döndürülmesi söz konusu. Bildiğiniz üzere sinatra şarkılarını varsayılan olarak 4567 numaralı porttan söylemekte. Bu senaryoda 8080 portunu kullanmak için küçük bir atama söz konusu. Talep sonrası içerik tipinin application/json formatında olacağı da content_type ataması ile belirleniyor. Gelelim özellikle Google Cloud Platform tarafındaki sistem için önem arz eden diğer iki içeriğe.

app.yaml isimli bir dosya oluşturup içeriğini aşağıdaki gibi doldurmam gerekti (Google dokümantasyonu sağolsun)

runtime: ruby
env: flex
entrypoint: bundle exec ruby app.rb

manual_scaling:
  instances: 1
resources:
  cpu: 1
  memory_gb: 0.5
  disk_size_gb: 10

Burada Google Cloud tarafındaki çalışma ortamı için gerekli bir takım ayarların bildirimi yapılıyor. Çalışma zamanının ruby motoruna göre tasarlanması gerektiği, uygulamanın başlatılacağı giriş noktası, ölçekleme seçenekleri ve donanımsal kaynak gereksinimleri(işlemci adedi, bellek, disk boyutu) gibi bilgilere yer veriliyor. Gemfile isimli uzantısız bir dosya daha eklemek lazım. Bu dosyada projenin ihtiyaç duyduğu gem'ler varsa isimleri bildiriliyor. source ataması ile de, ilgili gem içeriklerinin hangi kaynaktan çeklileceği söylenmekte. Paketler için depo bildirimi yapıldığını düşünebiliriz. Benim örneğimde sadece sinatra gerektiğinden içeriği aşağıdaki gibi yazmam yeterliydi.

source "https://rubygems.org"

gem "sinatra"

Bu işlemlerin arındandan ilk olarak kodun kendi sistemimde çalıştığından emin olmalıydım. Terminalden

ruby app.rb

ifadesi ile programı çalıştırdım ve tarayıcıyı her güncelleyişimde rastgele bir söz ile karşılaştığımı gördüm. Dikkat edeceğiniz üzere localhost:8080/quotes/random adresine talepte bulunuluyor. Şu an için yeterli gibi görünse de amaç bunu Google Cloud üzerinden sunabilmekti.

gcloud ile Taşıma

Sırada taşıma öncesi yapmam gereken bir hazırlık daha var(mış). Ruby kodunu tamamladıktan sonra özellikle

bundler install

terminal komutu ile proje bağımlılıkların yükleneceği Gemfile.lock'un oluşması gerekiyor. Ancak bu işlem sonrası taşıma adımlarına geçilebilir. Aksi halde taşıma işlemi sırasında hata alınmakta(Ne olduğunu söylesem mi? Yok, söylemeyeceğim...bundler install yapmadan gcloud deploy komutunu çalıştırıp kendiniz görün)

İçerik proje için aşağıdaki gibi oluştu.

GEM
  remote: https://rubygems.org/
  specs:
    mustermann (1.0.1)
    rack (2.0.3)
    rack-protection (2.0.0)
      rack
    sinatra (2.0.0)
      mustermann (~> 1.0)
      rack (~> 2.0)
      rack-protection (= 2.0.0)
      tilt (~> 2.0)
    tilt (2.0.8)

PLATFORMS
  ruby

DEPENDENCIES
  sinatra

BUNDLED WITH
   1.11.2

Artık taşıma işlemini deneyebilirdim.  

gcloud deploy

terminal komutunu çalıştırarak süreci başlattım. Bir kaç dakika sonrasında taşıma işleminin başarılı bir şekilde yapıldığını gördüm(Belki de Avusturalya'yı seçmemeliydim. Uzak diye mi taşıma böyle uzun sürdü dersiniz? :P) Bundan sonra tek yapmam gereken https://yourfunnyquote.appspot.com/quotes/random adresine gitmek oldu. Tabii dilerseniz komut satırından

gcloud app browse

diyerekten de tarayıcının açılmasını ve ilgili projeye gidilmesini sağlayabilirsiniz. Benim elde ettiğim sonuç şöyleydi.

Adres çubuğundaki adrese dikkat edin lütfen. Bu arada yazıyı tamamladıktan sonra servisi kapattım :)) Yani çalıştığının tek ispatı bu ekran görüntüsü. Nitekim Google Cloud Platform bana 364 günlük bedava kullanma süresi ve 300 dolarlık kredi bahşetmişti ama neme lazım. Her an her şey olabilirdi. Bu nedenle ilgili adrese ulaşamazsanız lütfen beni affedin. Kendi projeniz ile denediğinizde ekstra bir durum oluşmazsa taşımanızı sorunsuz gerçekleştiriyor olmalısınız.

Kapatırken

Pek tabii Google Cloud Platform bu kadar basit değil. Computation, Big Data, Storage, StackDriver, Networking vb pek çok ana başlık altında yer alan hizmetler içeriyor. Söz gelimi API Endpoint dikkatimi çekelerinden birisi. Özellikle ölçekleme ve yüksek performans gerektiren APIlerimiz için biçilmiş kaftan gibi duruyor. Bunun en büyük sebeplerinden birisi Proxy Container tarafında NGinx sunucularını kullanması. Aşağıdaki çizimde söz konusu yapının kabataslak hali var(Çözünürlük için lütfen kusura bakmayın. Telefon kamerasından ancak bu kadar oluyor)İstemci talepleri öndeki Load Balancer'dan sonra Nginx tabanlı Proxy servisine geliyor ve buradan API'nin(söz gelimi Python veya bir Java uygulamasının) konuşlandırıldığı Container Instance'ına geçiyor. gcloud alt taraftan da görüleceği üzere dağıtım adımından sorumlu. Servis yönetimi(Service Management) gcloud ile konuşarak taşıma operayonlarını ve konfigurasyon içeriğini yönetiyor. Çalışma zamanı kontrolleri ve raporlamalarda servis kontrolün(Service Control) işi. Çalışma zamanını web tabanlı Cloud Console üzerinden de izleme şansımız var. 

Bu mimarileri anlamaya çalışmamız önemli. Ama biraz dinlendikten sonra. Çünkü, West-World'de güneş çoktan battı. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

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.

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.

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.

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.

Viewing all 351 articles
Browse latest View live