Fırat Esmer

ANLATILAN SENİN HİKAYENDİR - Karl Marx

C# 7 - İlk Bakış

C# History

Visual Studio 2017'nin yayınlanması ile birlikte C# 7.0 da aramıza katılmış oldu. VS2017 RTM'e (son/kararlı hal) geçmeden önce dil hakkında bilgileri zaten ediniyor hatta kullanıyoruz. C# 7 ile gelen yeniliklerin yaşantımızda kullanacak kıvama gelmeden hemen önceki turumuza başlayalım.

Bahsedeceğim konular

  • Out Variables
  • Pattern Matching,
  • Tuples,
  • Local Functions,
  • Literals, Ref. Returns, Exceptions

Out Variables

  • Önceden tanımlamada var kullanılamıyor (int.TryParse(input, out var answer)),
  • Kullanmadan önce tanımlanması gerekiyor (demo),

Yukarıdaki sorunlara çözüm olarak Out Variables sunuldu.

// Eski yöntem
private static void Run()
{
	string name;
	string lastName;

	GetName(out name, out lastName);
	Console.WriteLine($"{name} {lastName}");
}

// C# 7.0 ile gelen yeni yöntem
private static void Run2()
{
	GetName(out string name, out string lastName);
	Console.WriteLine($"{name} {lastName}");
}

private static void GetName(out string name, out string lastName)
{
	name = "Fırat";
	lastName = "Esmer";
}

Pattern Matching

Pattern'in tanımı için şöyle güzel bir yorum var:

Syntactic elements that can test that a value has a certain "shape".

Bir değerin belirli bir şekle sahip olduğunu test edebilen sözdizimsel öğeler.

static void Main(string[] args)
{
	PrintSum(10);
	PrintSum2("10");
}

public static void PrintSum(object o)
{
	if (o is null) return; //Constant Pattern
	if (!(o is int i)) return; // Type Pattern (Int32)

	int sum = 0;

	for (int j = 0; j <= i; j++)
		sum += j;
}

public static void PrintSum2(object o)
{
	if (o is int i || o is string s && int.TryParse(s, out i))
	{
		int sum = 0;

		for (int j = 0; j <= i; j++)
			sum += j;
	}
}

Koşullu switch'e de bir göz atalım, burada gelen yenilik case durumunda koşul girebiliyor olmamız. Yani test edebiliyor olmamız.

class Program
{
	static void Main(string[] args)
	{
		Employee employee = new President();
		employee.Salary = 200000;
		employee.Years = 10;
		(employee as President).ManagedEmployeeNumber = 500;
		(employee as President).StockShares = 12000;

		// Switch içerisindeki sıralama önemli
		// Manager Employee'nin altında olsaydı hata alırdık
		// Kalıtım alan sınıf alınan sınıftan üstte olmalı
		switch (employee)
		{
			case President p when (p.StockShares < 10000):
				Console.WriteLine($"Düşük profilli yönetici hisse senedi payı : {p.StockShares}");
				break;

			case President p when (p.StockShares >= 10000):
				Console.WriteLine($"Yüksek profilli yönetici hisse senedi payı : {p.StockShares}");
				break;

			case Manager m:
				Console.WriteLine($"Yönetiye bağlı kişi : {m.ManagedEmployeeNumber}");
				break;

			case Employee e:
				Console.WriteLine($"Çalışan maaşı : {e.Salary}");
				break;
		}
	}
}

public class Employee
{
	public int Salary { get; set; }
	public int Years { get; set; }
}

public class Manager : Employee
{
	public int ManagedEmployeeNumber { get; set; }
}

public class President : Manager
{
	public int StockShares { get; set; }
}

Tuples

Metotlardan birden fazla değeri geri döndürmek istediğimizde aklımıza gelen en uygun yöntem out parametresi. Fakat out parametresinin de elimizi kolumuzu bağladığı bazı noktalar var. Mesela okunabilirliği zayıf, async metotlarla kullanılamıyor. Burada yardımımıza Tuple (System.Tuple<T>) yetişiyor. Tuple'ı başka bir Tuple'a convert edebilirsiniz. Tuple value type'dır (referans değil).

Öncelikle Tuples'ı kullanmak için NuGet Package kullanmamız gerektiğini belirteyim. Aşağıdaki resimde gördüğünüz gibi yüklemeyi gerçekleştirin.

System.Tuples Nuget Package

static void Main(string[] args)
{
	var numbers = GetThreeNumbers();
	// İsimlendirme işlemini biz yapmadığımız için
	// kendisi otomatik olarak yapıyor
	Console.WriteLine($"{numbers.Item1},{numbers.Item2},{numbers.Item3}");

	var numbersWithNames = GetThreeNumbersWithNames();
	Console.WriteLine($"{numbersWithNames.number1},{numbersWithNames.number2},{numbersWithNames.number3}");
}

public static (int, int, int) GetThreeNumbers()
{
	return (1, 56, 187);
}

public static (int number1, int number2, int number3) GetThreeNumbersWithNames()
{
	return (1, 56, 187);
}

Tuple ile Dictionary kullanımı

var tupleDictionary = new Dictionary<(int, int), string>();
tupleDictionary.Add((16, 21), "İki kardeşin yaşları");

// Sonuç = İki kardeşin yaşları
var result = tupleDictionary[(16, 21)];

Tuple Deconstruction örneği

static void Main(string[] args)
{
	(int number1, int number2, int number3) = GetThreeNumbers();
	Console.WriteLine($"{number1},{number2},{number3}");

	// Diğer kullanım şekli
	int _number1;
	int _number2;
	int _number3;
	(_number1, _number2, _number3) = GetThreeNumbers();
	Console.WriteLine($"{_number1},{_number2},{_number3}");
}

public static (int, int, int) GetThreeNumbers()
{
	return (1, 56, 187);
}

Local Functions

Local function, metot içerisinde metot kullanımıdır. Direkt örnekle açıklayayım, daha kolay olacaktır.

NOT : Örnekte Tuples kullanıldığı için Tuples başlığındaki gibi yükleme işlemini yapmanız gerekiyor.

static void Main(string[] args)
{
	// Fibonacci => 1, 1, 2, 3, 5, 8
	// Mevcut değerin bir önceki değer ile toplamı bir sonraki
	// değeri vermekte
	Console.WriteLine(Fibonacci(6));
	Console.Read();
	// Sonuç (6-1) + 8 = 13
}

public static int Fibonacci(int x)
{
	if (x < 0)
		throw new ArgumentException("Değer en az sıfır olmalı",
									nameof(x));

	return Fib(x).current;

	(int current, int previous) Fib(int i)
	{
		if (i == 0) return (1, 0);
		var (current, previous) = Fib(i - 1);
		return (current + previous, current);
	}
}

Literals, Ref. Returns, Exceptions

static void Main(string[] args)
{
	// Sonuç => 5781231
	Console.WriteLine(GetNumber());

	// Referans Return
	int[] numbers = { 1, 3, 5, 7, 9, 11 };
	ref int position = ref Substitute(5, numbers);
	position = -30;
	Console.WriteLine(numbers[2]);

	// Sonuç => Fırat
	Employee employee = new Employee("Fırat");
	Console.WriteLine(employee.Name);
	// Sonuç => Hata / Exception
	Employee employee2 = new Employee(null);
	Console.WriteLine(employee.Name);

	Console.Read();
}

// Literal: Altçizgi (underscore) seperator (ayırıcı)
// olarak değil dönüş tipi olarak dönüyor
private static int GetNumber()
{
	return 5_7_8_123_1;
}

// Reference Return örneği
private static ref int Substitute(int value, int[] numbers)
{
	for (int i = 0; i < numbers.Length; i++)
		if (numbers[i] == value)
			return ref numbers[i];

	throw new IndexOutOfRangeException("Bulunamadı!");
}

// Exception as expression örneği
public class Employee
{
	public string Name { get; }
	public Employee(string name) => Name = name ?? throw new ArgumentNullException();
}

Son olarak

C# 7 yenilikler listesini Microsoft Documents üzerinden incelemek için => https://docs.microsoft.com/en-us/dotnet/articles/csharp/whats-new/csharp-7

.NET Dünyasında Kriptografi

Kriptografi Nedir? Neden ve Nerede Kullanılır?

Kriptografi, en geniş anlamıyla bilgileri gizlemektir. Fiziksel dünyadaki bilginin elektronik dünyaya taşınması ile ortaya çıkan güvenlik endişelerini gidermek için kullanıldığında söz bize, yazılımcılara düşer. Hepimiz gündelik hayatta güvenlik önlemleri altında hayatımıza devam ediyoruz. Bilgisayarınızı açtığınızda kim olduğunuzu kanıtlamak veya alışveriş sitesinde kredi kartı ile alışveriş yapmak bunlara verilebilecek en basit örneklerdir. Her iki işlemde de belirli parametreler (kredi kartı no. veya kullanıcı adı ve parolası) belirli adreslere gönderilip kontrol edilirken şifrelenirler.

Encryption & Decryption

Gizleme işlemine şifreleme diğer bir adıyla da encryption, şifrelenen mesajın şifresinin kaldırılmasına (okunması) ise decryption denir.
Örneğin, size bir mesaj gönderildiğini, bu mesajın şifrelendiğini ve anahtarın sadece mesajı gönderen kişide ve sizde olduğunu düşünün. Böylece mesaj vericinin elinden çıktığı andan alıcının eline ulaştığı ana kadar şifrelenmiş olacaktır. (Aynı şifreleme algoritmasının kullanıldığı bu işleme simetrik anahtar algoritması diğer adıyla symmeric encryption denir. İleride değineceğim.)

Encryption

Mesajın gönderilmeden önce şifrelenmesi

Decryption

Mesajın alıcıya ulaşmadan önce şifrenin kaldırılması

Kriptografinin Ana Konseptleri

  • Gizlilik: Yaptığınız işin içeriğinde kullanılan parametrelerin ne olduğunun asla bilinmemesi,
  • Bütünlük: İşlemin başından sonuna kadar şifrelenmiş verinin değişime uğramadan gideceği adrese ulaşması,
  • Reddedilmeme: Şifrelemeyi gerçekleştiren kişinin doğrulama adına imzasının olması,
  • Kimlik: Gizliliğin olduğu herhangi bir ortama erişim için kanıtlanmış kimlik.

> Rastgele Sayılarla Kriptografi

  • Şifreleme anahtarı oluşturmak için kullanılır,
  • Bazı sistemlerde cihazın sahip olduğu donanım bilgisi şifreleme işlemlerinde kullanılır. Mesela MAC ethernet adresi gibi. Donanımda bunun kullanılamadığı durumlar da söz konusu. Burada devreye yazılım tabanlı şifreleme girmeli. Bu yöntem için tamamen "rastgele" denemez,
  • Rastgelelik durumu insan etkileşimi ile oluşturulabilir,
  • Sunucu uygulamaları için uygun değildir çünkü manipülasyona açıktır. O yüzden belirli bir algoritma ya da donanıma ait bilgiler kullanılmalıdır.

Donald. E.Knuth adlı bilgisayar bilimcinin "Subtractive Random Number Generator" çalışması temel alınmıştır. Daha fazla bilgi için => The Art of Computer Programming

            // Farklı .NET Framework versiyonlarında
            // farklı değerler üretir (aynı seed değerler
            // olsa da)

            // Deterministtir, öngörülebilir

            Random rnd = new Random(350);

            for (int i = 0; i < 10; i++)
                Console.Write("{0,3}   ",rnd.Next(-10, 11));

            Console.Read();

System.Random ve Sorunları

  • Microsoft, uygulamanızda tek bir System.Random sınıfı instance'ı kullanılmasını öneriyor. Bkz. Random Class,
  • System.Random thread safe değildir (tek thread kullanımına dikkat edin aksi takdirde 0 değeri dönebilir),

Rastgele Numaraları RNGCryptoServiceProvider sınıfı ile kullanırsanız daha güvenli şifreleme işlemleri gerçekleştirirsiniz. System.Random'a göre performans açısından daha yavaştır fakat tamamen rastgeledir.

        static void Main(string[] args)
        {
            for (int i = 0; i < 10; i++)
                // Base64 işlemi daha okunabilir hale gelmesi için uygulanıyor
                Console.WriteLine($"Rastgele Numara {i + 1}: {Convert.ToBase64String(GenerateRandomNumber(32))}");

            Console.ReadLine();
        }

        public static byte[] GenerateRandomNumber(int length)
        {
            // 256-bit şifreleme için
            // Length = 32 (32 byte * 8 bit = 256 bit)
            using (var randomNumberGenerator = new RNGCryptoServiceProvider())
            {
                var randomNumber = new byte[length];
                randomNumberGenerator.GetBytes(randomNumber);

                return randomNumber;
            }
        }

RNGCryptoServiceProvider

> Hashing Algoritmaları İle Kriptografi

  • Verilen parametreyi kolayca hash'leyip hazırlar,
  • Hash'lenmiş bir veriyi ilk haline çevirmek için kullanılamaz, tek yönlüdür,
  • Hash'lenmiş verinin, orijinal halinde yapılacak en ufak bir değişiklik, tamamen farklı bir değerin üretilmesine sebep olacaktır,
  • İki farklı parametre aynı hash değeri üretemez (parmak izi örnek verilebilir, eşi yoktur),

.NET uygulamarında kullanılabilecek hash çeşitleri

Hashing Algorithms

  • Hashing ile geri döndürme işlemi yapılamaz. Yani, şifreli bir mesajı tekrar orijinal parametre haline çeviremezsiniz. Encryption'da bu mümkün. Yön farkları buradan gelmektedir,
  • Orijinal parametre değişmedikçe hash işlemi yapıldıkça üretilecek değerler aynı olacaktır (tek bir karakteri bile değişse, çok farklı bir değer üretileceğini belirtmiştik),

MD5

  • 1991 yılında MD4'un yerini almıştır,
  • 128-bit,
  • 1996 yılında ilk açık bulundu, 2004 yılında bulunan açıkların sayısı arttı. Kriptografcılar SHA gibi diğer yöntemlerin kullanılmasını öneriyor (çok nadir de olsa iki farklı veri seti aynı hash değer üretebiliyordu),
  • Yine de eski uygulamalarda kullanılmaktadır.

SHA (Secure Hash Algorithm)

  • SHA-1, NSA (National Security Agency) tarafından geliştirildi. 160 bit uzunluktadır. Bulunan sorunlardan ötürü daha fazla gelişimi sürmedi,
  • SHA-2, SHA-256 ve SHA-512 ailelerini temsil eder. SHA-256 256-bit, SHA-512 542-bit. NSA tarafından geliştirilmiştir,
  • SHA-3, 2012 yılında non-NSA yarışması sonucu doğmuştur. SHA-2'nin sahip olduğu hash uzunluğuna sahiptir. .NET Framework tarafından desteklenmemektedir. Üçüncü parti yazılımlarla ekleme yapmak mümnkün (.NET Framework desteği olmadığı için üzerinde durmayacağım).
        // Aşağıda belirtilen kriptografi sınıfları aynı
        // Interface'i kullanmakta => HashAlgorithm

        public static byte[] ComputeHashSHA1(byte[] toBeHashed)
        {
            using (var sha1 = SHA1.Create())
                return sha1.ComputeHash(toBeHashed);
        }

        public static byte[] ComputeHashSHA256(byte[] toBeHashed)
        {
            using (var sha256 = SHA256.Create())
                return sha256.ComputeHash(toBeHashed);
        }

        public static byte[] ComputeHashSHA512(byte[] toBeHashed)
        {
            using (var sha512 = SHA512.Create())
                return sha512.ComputeHash(toBeHashed);
        }

        public static byte[] ComputeHashMD5(byte[] toBeHashed)
        {
            using (var md5 = MD5.Create())
                return md5.ComputeHash(toBeHashed);
        }

Hashed Message Authentication Codes (HMAC)

Diğer adı hash MAC olan yöntemle verinizin bütünlüğü yine bir anahtar aracılığıyla kontrol edilebilir. Anahtar (input) kullanılarak şifrelenmiş veriniz, aynı anahtar kullanılarak tekrar üretilecek şifre değeri ile aynı olur ve tutarlılık kontrolü yapılabilir. Temel seviyedeki hash şifreleme sistemine göre daha az etkilidir. MD5 veya SHA ailesi kullanılarak şifreleme yapılır. Bu şifreleme yöntemine karşı en çok yapılan saldırı tipi Brute Force'tur.

HMAC

private const int KeySize = 64;

public static byte[] GenerateKey()
{
	using (var randomNumberGenerator = new RNGCryptoServiceProvider())
	{
		var randomNumber = new byte[KeySize];
		randomNumberGenerator.GetBytes(randomNumber);

		return randomNumber;
	}
}

//HMACxx sınıfına key verilmezse tamamen rastgele değer üretir

/*HMACxx sınıfına verilecek parametrenin uzunluğu olarak 64 bit
 kullanılması öneriliyor. Düşükse tamamlanır, fazlaysa kırpılır.
*/

public static byte[] ComputeHMACSHA1(byte[] toBeHashed, byte[] key)
{
	using (var hmac = new HMACSHA1(key))
		return hmac.ComputeHash(toBeHashed);
}

public static byte[] ComputeHMACSHA256(byte[] toBeHashed, byte[] key)
{
	using (var hmac = new HMACSHA256(key))
		return hmac.ComputeHash(toBeHashed);
}

public static byte[] ComputeHMACSHA512(byte[] toBeHashed, byte[] key)
{
	using (var hmac = new HMACSHA512(key))
		return hmac.ComputeHash(toBeHashed);
}

public static byte[] ComputeHMACMD5(byte[] toBeHashed, byte[] key)
{
	using (var hmac = new HMACMD5(key))
		return hmac.ComputeHash(toBeHashed);
}

> Şifre Depolama Yöntemleri

  • Şifresiz Kayıt

Kesinlikle uygulanmaması gereken depolama şekli. Eğer şifrelenmesi gereken veriniz şifresiz bir şekilde yolculuk ediyor ve gideceği noktada yine şifrelenmemiş bir şekilde kayıt altında tutuluyorsa, sisteminizde çok büyük güvenlik zafiyeti vardır. Sisteme erişim halinde hassas verileriniz çıplak bir şekilde sergileniyor olacak (plain text).

Clear Text Storage

  • Encryption

Uygulamalarımızda hassas verilerin veritabanına şifreli olarak kaydedilmesi, veritabanına erişim sonrası yaşanacak sorunları bir nebze azaltacaktır. Fakat encrypt edilen bir verinin, tekrar kullanımı için decrypt edilmesi lazım. Yani şifre oluşturulurken kullanılan anahtar sözcüğün (key) de yönetimi söz konusu. Veritabanına kaydedilmiş şifreli verinin decrypt edilememesi gerekir. Çünkü anahtar çalınırsa tüm kilitler açılır.

Encryption

  • Hash

Encryption'dan daha güvenilir ve kullanışlı olan hash yönteminin de kendince sorunlar var. Önce artılarına bakalım; hash işlemi geri çevrilemediği için şifrelenmeden önceki halinin ne olacağı bilinemiyor böylece sisteme sızılması durumunda veriler kötü niyetli kişiler için anlamsız oluyor. Ayrıca key kullanılmadığı, tamamen rastgele değerler üretildiği için key management gibi bir sorun da yok.

  1. Brute Force Attack : Saldıran kişi, farklı kombinasyonlarla üretilmiş hash değerini sisteminizde kaydedilmiş herhangi bir kayıtla eşleşmesi için sürekli deneyecektir. Kulağa eşleşmesi neredeyse imkansızmış gibi geldiğinin farkındayım fakat teknolojinin nimetlerinden yararlanarak milyonlarca kaydı çok kısa süre içerisinde çoktan eşleştirmiş oluyorlar (o yüzden kullanılmış, herkes tarafından bilinen 12345 gibi şifreleri kullanmayın).
  2. Rainbow Table Attack :  Farklı şifrelerin hashlenmiş hallerini üzerinde bulunduran büyük bir kaynak (key-value ve GB'larca olması mümkün) ve bu kaynaktan sisteme girilmeye çalışıldığını düşünün,
  • Salted Hashes

Hash kullanarak yarattığınız değerlere kendi kombinasyonlarınızı da uygulayabilirsiniz. Mesela hash'lemek istediğiniz parametrenin byte dizisi ile yine kendi yarattığınız rastgele (random) sayıların byte dizisini birleştirebilir (combine) ve bu birleşimi hash'leyebilirsiniz. Böylece Brute Force ve Rainbow Table saldırılarında önceden hashlenmiş değerlerin sizinki ile uyuşması, ya da tahmin edilebilir olması zorlaşacaktır.

Password Salt Hashing

public static byte[] GenerateSalt()
{
	const int saltLength = 32;

	using (var randomNumberGenerator = new RNGCryptoServiceProvider())
	{
		var randomNumber = new byte[saltLength];
		randomNumberGenerator.GetBytes(randomNumber);

		return randomNumber;
	}
}

private static byte[] Combine(byte[] first, byte[] second)
{
	var value = new byte[first.Length + second.Length];

	Buffer.BlockCopy(first, 0, value,0, first.Length);
	Buffer.BlockCopy(second, 0, value, first.Length, second.Length);

	return value;
}

private static byte[] HashPasswordWithSalt(byte[] toBeHashed, byte[] salt)
{
	using (var sha256 = SHA256.Create())
	{
		return sha256.ComputeHash(Combine(toBeHashed, salt));
	}
}
  • Password-Based Key Derivation Function (PBKDF2)

Hash fonksiyonun salt ile birlikte kullanımı bile bizim için yeteri kadar güvenli bir ortam oluşturmuyor. Bunun sebebi ise her geçen gün işlemcilerin veya bilgisayarların giderek daha da hızlanması ile Brute Force ve Rainbow Table saldırılarının tehlikeli boyutlara gelmiş olması (bkz. Moore Yasası) Haliyle bize daha güvenli bir yöntem lazım. Burada devreye RSA Public Key Cryptographic Standards serisi devreye giriyor. Diğer adı Internet Engineering Task Force's RFC 2898'dır.

PBKDF, parametreyi (şifrelenecek veri, password) alır, üstüne salt ekler daha sonra ise belirtilen sayıda algoritmanın üreteceği değeri daha da karmaşık hale getirir. Böylece saldırılara karşı daha da güvenilir, karmaşık ve denendiği halde eşleşmesi çok daha uzun süre alacak bir değer üretilecektir. (LastPass, şifrelerinizi tek bir yerde toplayan ve yönetilebirliği artıran bir uygulama. JavaScript client için 5bin, server-side için 100bin iterasyon kullanmış bkz. LastPass Password Iterations (PBKDF2))

NOT

  1. 64bit (8byte) salt uzunluğu öneriliyor,
  2. Sisteminizi performans açısından zorlamayacak sayıda yineleme (iterasyon) işlemi yapılmalı,
  3. Moore Yasası baz alınırsa, her iki senede yineleme sayısını iki kat artıracak şekilde işlemlerinizi gerçekleştirin,
  4. Salt hiçbir yöntemde gizli olma zorunluluğu taşımaz.

PBKDF2

static void Main(string[] args)
{
	string password = "Kompleks Şifre Örneği";

	HashPassword(password, 100);
	HashPassword(password, 1000);
	HashPassword(password, 10000);
	HashPassword(password, 100000);

	Console.Read();
}

public static byte[] GenerateSalt()
{
	using (var randomNumberGenerator = new RNGCryptoServiceProvider())
	{
		var randomNumber = new byte[32];
		randomNumberGenerator.GetBytes(randomNumber);

		return randomNumber;
	}
}

private static void HashPassword(string passwordToHash, int iterationNumber)
{
	Stopwatch stopwatch = new Stopwatch();
	stopwatch.Start();

	var hashedPassword = HashPassword(Encoding.UTF8.GetBytes(passwordToHash), GenerateSalt(), iterationNumber);

	stopwatch.Stop();

	Console.WriteLine($"Parametre : {passwordToHash}");
	Console.WriteLine($"Parametrenin Hash'li Hali : {Convert.ToBase64String(hashedPassword)}");
	Console.WriteLine($"Yineleme sayısı : {iterationNumber}, geçen süre : {stopwatch.ElapsedMilliseconds} ms");
	Console.WriteLine();
}

private static byte[] HashPassword(byte[] password, byte[] salt, int iterationNumber)
{
	// Rfc2898DeriveBytes sınıfı PBKD fonksiyonudur
	using (var rfc2898 = new Rfc2898DeriveBytes(password, salt, iterationNumber))
		return rfc2898.GetBytes(32);
}

Çıktısı =>

PBKDF2 Örnek Çıktısı

> Simetrik Şifreleme (Symmetric Encryption)

Çift yönlü şifrelemede kısaca değindik, birazdan farklı şifreleme tekniklerine bakacağız ama yine de kısaca değinelim.

Hızlı, güvenilir fakat yine ortada anahtar olan, anahtar ile tüm şifrelemelerin çözülebildiği bir şifreleme türü. Tüm simetrik şifreleme sınıfları SymmetricAlgorithm abstract sınıfını miras alır. Hızlı ve güvenlidir. Encrypt ve decrypt işlemlerinde aynı anahtarı kullanıyor olmasından ötürü simetrik denmektedir.

Simetrik Şifreleme

  • Data Encryption Standard (DES)
    IBM tarafından geliştirilmiştir. 64 bitlik key vardır fakat algoritma 56 bitlik kısmını kullanır. Güvenilirliğini test etmek için açılan meydan okuma yarışmasının (DESCHALL) 96. gününde şifre kırılmıştır. (Yarışma ve yaşanan hack olayı için daha fazla bilgiyi bu kitapta bulabilirsiniz => Brute Force: Cracking the Data Encryption Standard)

    DES
    static void Main(string[] args)
    {
    	string password = "Kompleks Şifre Örneği";
    
    	var key = GenerateRandomNumber(8);
    	var iv = GenerateRandomNumber(8);
    
    	var encrypted = Encrypt(Encoding.UTF8.GetBytes(password), key, iv);
    	var decrypted = Encoding.UTF8.GetString(Decrypt(encrypted, key, iv));
    
    	Console.WriteLine($"Orijinal hali : {password}");
    	Console.WriteLine($"Şifrelenmiş hali : {Convert.ToBase64String(encrypted)}");
    	Console.WriteLine($"Şifresi çözülmüş hali : {decrypted}");
    
    	Console.Read();
    }
    
    private static byte[] GenerateRandomNumber(int length)
    {
    	using (RNGCryptoServiceProvider randomNumberGenerator = new RNGCryptoServiceProvider())
    	{
    		var randomNumber = new byte[length];
    		randomNumberGenerator.GetBytes(randomNumber);
    
    		return randomNumber;
    	}
    }
    
    private static byte[] Encrypt(byte[] dataToEncrypt, byte[] key, byte[] iv)
    {
    	using (DESCryptoServiceProvider des = new DESCryptoServiceProvider())
    	{
    		des.Key = key;
    		des.IV = iv;
    
    		using (MemoryStream memoryStream = new MemoryStream())
    		{
    			CryptoStream cryptoStream = new CryptoStream(memoryStream, des.CreateEncryptor(), 
    										CryptoStreamMode.Write);
    
    			cryptoStream.Write(dataToEncrypt, 0, dataToEncrypt.Length);
    			cryptoStream.FlushFinalBlock();
    
    			return memoryStream.ToArray();
    		}
    	}
    }
    
    private static byte[] Decrypt(byte[] dataToDecrypt, byte[] key, byte[] iv)
    {
    	using (DESCryptoServiceProvider des = new DESCryptoServiceProvider())
    	{
    		des.Key = key;
    		des.IV = iv;
    
    		using (MemoryStream memoryStream = new MemoryStream())
    		{
    			CryptoStream cryptoStream = new CryptoStream(memoryStream, des.CreateDecryptor(), 
    											CryptoStreamMode.Write);
    
    			cryptoStream.Write(dataToDecrypt, 0, dataToDecrypt.Length);
    			cryptoStream.FlushFinalBlock();
    
    			return memoryStream.ToArray();
    		}
    	}
    }
    DES

  • Triples DES
    DES'in güvenilirliği sorgulanmaya başladıktan sonra yeni bir şifreleme algoritmasının temelleri de atılmış oldu. DES'e göre en büyük farklılığı yine DES'i uyguluyor olması fakat üstüne farklı 2-3 anahtarın ekleniyor olması. Birinci adımda anahtar ile oluşturulan şifreye, ikinci anahtar eklenerek tekrardan şifreleniyor (2. veya 3. anahtar. Şifrelenmiş halinden orjinal haline geri döndürmek için kullandığınız anahtar sırasını tersten işlemeniz gerekiyor.)

    Triple DES
    static void Main(string[] args)
    {
    	string password = "Kompleks Şifre Örneği";
    
    	// 3 key kullanımı
    	// 24 / 8 = 3 tane 64bit anahtar
            // 32 verirseniz hata alırsınız. Maksimum : 3 anahtar
    	// UNUTMA : 56bit kullanılıyor
    	var key = GenerateRandomNumber(24);
    	var iv = GenerateRandomNumber(8);
    
    	var encrypted = Encrypt(Encoding.UTF8.GetBytes(password), key, iv);
    	var decrypted = Encoding.UTF8.GetString(Decrypt(encrypted, key, iv));
    
    	Console.WriteLine($"Orijinal hali : {password}");
    	Console.WriteLine($"Şifrelenmiş hali : {Convert.ToBase64String(encrypted)}");
    	Console.WriteLine($"Şifresi çözülmüş hali : {decrypted}");
    
    	Console.Read();
    }
    
    private static byte[] GenerateRandomNumber(int length)
    {
    	using (RNGCryptoServiceProvider randomNumberGenerator = new RNGCryptoServiceProvider())
    	{
    		var randomNumber = new byte[length];
    		randomNumberGenerator.GetBytes(randomNumber);
    
    		return randomNumber;
    	}
    }
    
    private static byte[] Encrypt(byte[] dataToEncrypt, byte[] key, byte[] iv)
    {
    	using (TripleDESCryptoServiceProvider tripleDES = new TripleDESCryptoServiceProvider())
    	{
    		tripleDES.Key = key;
    		tripleDES.IV = iv;
    
    		using (MemoryStream memoryStream = new MemoryStream())
    		{
    			CryptoStream cryptoStream = new CryptoStream(memoryStream, tripleDES.CreateEncryptor(), 
    											CryptoStreamMode.Write);
    
    			cryptoStream.Write(dataToEncrypt, 0, dataToEncrypt.Length);
    			cryptoStream.FlushFinalBlock();
    
    			return memoryStream.ToArray();
    		}
    	}
    }
    
    private static byte[] Decrypt(byte[] dataToDecrypt, byte[] key, byte[] iv)
    {
    	using (TripleDESCryptoServiceProvider tripleDES = new TripleDESCryptoServiceProvider())
    	{
    		tripleDES.Key = key;
    		tripleDES.IV = iv;
    
    		using (MemoryStream memoryStream = new MemoryStream())
    		{
    			CryptoStream cryptoStream = new CryptoStream(memoryStream, tripleDES.CreateDecryptor(), 
    										CryptoStreamMode.Write);
    
    			cryptoStream.Write(dataToDecrypt, 0, dataToDecrypt.Length);
    			cryptoStream.FlushFinalBlock();
    
    			return memoryStream.ToArray();
    		}
    	}
    }
    Triple DES

  • Advanced Encryption Standard (AES)
    DES'in yerini alması için 2001 yılında kullanılmaya başlanmıştır. DES'in aksine Feistel Network kullanmaz. 128 bit girdi, 128, 192 veya 256 bitlik anahtar (sırasıyla 10, 12,14 yineleme yapar) kullanır. AES, en çok güvenilen şifreleme yöntemlerinden birisidir. Değiştirme-Karıştırma yöntemi ile çok karmaşık bir değer üretilir. Şifrenin kırılması için gerekli süre adına şuan bulunan süper bilgisayarlar kullanılsa bile evrenin yaşından daha fazla vakit alacağı söyleniyor. (256bit = 1.1x1077 ihtimal)

    AES

    static void Main(string[] args)
    {
    	string password = "Kompleks Şifre Örneği";
    
    	var key = GenerateRandomNumber(32);
    	var iv = GenerateRandomNumber(16);
    
    	var encrypted = Encrypt(Encoding.UTF8.GetBytes(password), key, iv);
    	var decrypted = Encoding.UTF8.GetString(Decrypt(encrypted, key, iv));
    
    	Console.WriteLine($"Orijinal hali : {password}");
    	Console.WriteLine($"Şifrelenmiş hali : {Convert.ToBase64String(encrypted)}");
    	Console.WriteLine($"Şifresi çözülmüş hali : {decrypted}");
    
    	Console.Read();
    }
    
    private static byte[] GenerateRandomNumber(int length)
    {
    	using (RNGCryptoServiceProvider randomNumberGenerator = new RNGCryptoServiceProvider())
    	{
    		var randomNumber = new byte[length];
    		randomNumberGenerator.GetBytes(randomNumber);
    
    		return randomNumber;
    	}
    }
    
    private static byte[] Encrypt(byte[] dataToEncrypt, byte[] key, byte[] iv)
    {
    	using (AesCryptoServiceProvider aes = new AesCryptoServiceProvider())
    	{
    		aes.Key = key;
    		aes.IV = iv;
    
    		using (MemoryStream memoryStream = new MemoryStream())
    		{
    			CryptoStream cryptoStream = new CryptoStream(memoryStream, aes.CreateEncryptor(), 
    										CryptoStreamMode.Write);
    
    			cryptoStream.Write(dataToEncrypt, 0, dataToEncrypt.Length);
    			cryptoStream.FlushFinalBlock();
    
    			return memoryStream.ToArray();
    		}
    	}
    }
    
    private static byte[] Decrypt(byte[] dataToDecrypt, byte[] key, byte[] iv)
    {
    	using (AesCryptoServiceProvider aes = new AesCryptoServiceProvider())
    	{
    		aes.Key = key;
    		aes.IV = iv;
    
    		using (MemoryStream memoryStream = new MemoryStream())
    		{
    			CryptoStream cryptoStream = new CryptoStream(memoryStream, aes.CreateDecryptor(), 
    										CryptoStreamMode.Write);
    
    			cryptoStream.Write(dataToDecrypt, 0, dataToDecrypt.Length);
    			cryptoStream.FlushFinalBlock();
    
    			return memoryStream.ToArray();
    		}
    	}
    }
    AES

> Asimetrik Şifreleme (Asymmetric Encryption)

Simetrik şifrelemenin hızlı ve güvenilir olduğundan bahsetmiştik fakat anahtar yönetimi sorunu mevcut. Anahtara sahip kişinin verebileceği hasar, tüm şifreleri kırabilmesiyle başlıyor. Bunun çözümü asimetrik şifrelemede. Asimetrik şifreleme de iki çeşit anahtar var. Birisi açık (public) diğeri ise kapalı (private). Bu iki anahtar birbiri ile (matematiksel olarak) bağlantılı ve sadece private anahtarın saklanması gerekiyor. Public key herkesin erişimine açıkken, private key sadece alıcıya aittir. Public key ile encrypt, private key ile decrypt yapılıyor. Peki private key'in ele geçirilmesi ile yaşanacak sorunlar nasıl önleniyor? Şöyle; private key'e sahip olan kişi, hangi public key'e bağlı olduğunu bilmiyor çünkü anahtar değişimi yok. Simetrik şifrelemeye göre dezavantajı ise işlemin daha yavaş olması. Sebebi ise daha kompleks bir yapıda olması.

Asimetrik Şifreleme

  • RSA (Rivest, Shamin ve Adelman)
    • İsmini, tekniğin mucitlerinden almaktadır. RSA Security LLC firmasının bir ürünüdür,
    • RSA, diğer simetrik şifrelemelerle kullanılabilir. Birazdan hibrit şifrelemede bu konuya değineceğim,
    • 1024, 2048 ve 4096 bit anahtar kullanır. Günümüz koşullarında en az 2048 bitlik anahtar kullanılması öneriliyor. 1024 bitlik anahtar zayıf görülüyor,
    • Açık ve kapalı anahtarlar asal sayı temellidir,
    • Encryption ve decryption işlemleri matematik operasyonlarından oluşuyor, yavaş olmasının sebebi de budur (modüler matematik, çarpımların ayrımı, iki asal sayının çarpımı).

      İki farklı kullanımı mevcut. İlki, XML çıktı alabileceğiniz (ToXmlString()) veya in-memory saklayabileceğiniz provider kullanmak diğeri ise Microsoft'un kendi konteyner (container) yapısını kullanmak. Böylece yaratılan key kullanıcıya bağlanabilir, kullanıcı silindiği zaman bu bilgiler de silinir. Diğer bir avantajı ise korumak istediğiniz bir uygulamayı, sistemi veya birden fazla uygulama grubunu korumak için bir kullanıcıya (örnek : admin) bunu bağlamak. NOT : Yaratılan keyler açık şekilde bilgisayarda tutulmamalı.
private RSAParameters _publicKey;
private RSAParameters _privateKey;

private void AssignKey()
{
	using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(2048))
	{
		// Konteyner kullanımı
		rsa.PersistKeyInCsp = false;
		// Public için parametre false
		_publicKey = rsa.ExportParameters(false);
		// Private için parametre true
		_privateKey = rsa.ExportParameters(true);
	}
}

private byte[] EncryptData(byte[] dataToEncrypt)
{
	byte[] cipherBytes;

	using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(2048))
	{
		rsa.PersistKeyInCsp = false;
		// Encrypt için public
		rsa.ImportParameters(_publicKey);

		cipherBytes = rsa.Encrypt(dataToEncrypt, false);
	}

	return cipherBytes;
}

private byte[] DecryptData(byte[] dataToDecrypt)
{
	byte[] plain;

	using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(2048))
	{
		rsa.PersistKeyInCsp = false;

		rsa.ImportParameters(_privateKey);
		// Decrypt için private
		plain = rsa.Decrypt(dataToDecrypt, true);
	}

	return plain;
}

               Konteyner kod örneği =>

const string ContainerName = "Konteyner";

private void AssignKey()
{
	CspParameters cspParams = new CspParameters(1);
	cspParams.KeyContainerName = ContainerName;
	cspParams.Flags = CspProviderFlags.UseMachineKeyStore;
	cspParams.ProviderName = "Microsoft Strong Cryptographic Provider";

	var rsa = new RSACryptoServiceProvider(cspParams) { PersistKeyInCsp = true };
}

private byte[] EncryptData(byte[] dataToEncrypt)
{
	byte[] cipherBytes;

	var cspParams = new CspParameters { KeyContainerName = ContainerName };

	using (var rsa = new RSACryptoServiceProvider(2048, cspParams))
		cipherBytes = rsa.Encrypt(dataToEncrypt, false);

	return cipherBytes;
}

private byte[] DecryptData(byte[] dataToDecrypt)
{
	byte[] plain;

	var cspParams = new CspParameters { KeyContainerName = ContainerName };

	using (var rsa = new RSACryptoServiceProvider(2048, cspParams))
		plain = rsa.Decrypt(dataToDecrypt, false);

	return plain;
}

// Konteynerdaki anahtarları silme işlemi
//private void DeleteKeyInCsp()
//{
//    var cspParams = new CspParameters { KeyContainerName = ContainerName };
//    var rsa = new RSACryptoServiceProvider(cspParams) { PersistKeyInCsp = true };

//    rsa.Clear();
//}

> Hibrid Şifreleme (Hybrid Encryption)

  • Simetrik şifrelemede anahtar paylaşımı riskli,
  • Asimetrik şifrelemede ise simetrik şifrelemeye göre anahtar paylaşımı daha güvenilir fakat işlem yavaş.

İki şifreleme yönteminin de dahil edildiği, daha güvenilir ve iyi bir çözüm sunmak adına hibrid şifreleme yapmak mümkün. Simetrik şifreleme anahtarının asimetrik şifreleme (RSA gibi) ile şifrelenmesi. Ortaya çıkan bu unique anahtara Session Key denir. Örnek için AES + RSA kodu yazılabilir, tekrar tekrar yazmamak adına pas geçiyorum.

> Dijital İmza (Digital Signature)

Kriptografinin ilkeleri konusunda bahsedilen reddedilmeme ilkesi dijital imza ile alakalıdır. Dijital imza, size, yaratılan mesajın bir sahibi olduğunu ve buna güvenmeniz için gerekli sebepleri barındırdığını belirten ibaredir. Kısacası dijital imza, uygun bir private key ile bir kullanıcı tarafından yaratıldığının en belirgin ve güvenilir ifadesidir. Asimetrik şifreleme tabanlıdır.

  • Public ve private key'lerden oluşur,
  • İmzalama işlemi private key ile gerçekleşir,
  • Doğrulama işlemi public key ile gerçekleşir.

.NET Framework'te dijital imza 3 sınıf kullanır:

  1. RSACryptoServiceProvider,
  2. RSAPKCS1SignatureFormatter,
  3. RSAPKCS1SignatureDeformatter.

Bu sınıflar verimizin doğruluğunu ve güvenilirliğini sağlar.

static void Main(string[] args)
{
	var document = Encoding.UTF8.GetBytes("Top Secret Document");
	byte[] hashedDocument;

	using (var sha256 = SHA256.Create())
		hashedDocument = sha256.ComputeHash(document);

	AssignKey();

	var signature = SignData(hashedDocument);

	// True olursa kodumuz sorunsuz çalışmış demektir
	// Verify oldu = true
	var isVerified = VerifySignature(hashedDocument, signature);

	Console.Read();
}

private static RSAParameters _publicKey;
private static RSAParameters _privateKey;

private static void AssignKey()
{
	using (var rsa = new RSACryptoServiceProvider(2048))
	{
		rsa.PersistKeyInCsp = false;
		_publicKey = rsa.ExportParameters(false);
		_privateKey = rsa.ExportParameters(true);
	}
}

private static byte[] SignData(byte[] hash)
{
	using (var rsa = new RSACryptoServiceProvider(2048))
	{
		rsa.PersistKeyInCsp = false;
		rsa.ImportParameters(_privateKey);

		var rsaFormatter = new RSAPKCS1SignatureFormatter(rsa);
		rsaFormatter.SetHashAlgorithm("SHA256");

		return rsaFormatter.CreateSignature(hash);
	}
}

private static bool VerifySignature(byte[] hash, byte[] signature)
{
	using (var rsa = new RSACryptoServiceProvider(2048))
	{
		rsa.ImportParameters(_publicKey);

		var rsaDeformatter = new RSAPKCS1SignatureDeformatter(rsa);
		rsaDeformatter.SetHashAlgorithm("SHA256");

		return rsaDeformatter.VerifySignature(hash, signature);
	}
}

Yukarıdaki örnekte verify işlemi sırasında hashedDocument değişkenindeki byte dizisi değerinden herhangi birisini değiştirmeniz sonucun false olmasına sebep olacaktır çünkü orijinal değerden farklı olacaktır.

NOT : Hibrid yöntemle kodunuzu genişletmeniz mümkün.

> SecureString

  • System.String kütüphanesi güvenli bir çözüm değil,
  • System.String kütüphanesi bazı sorunlar barındırmaktadır
    • Bellekte birden fazla kopya,
    • Şifreli değil,
    • Değiştirilebilir

Yukarıda belirtilen sorunlar bildiğimiz string veri tipinde tutulan hassas veriler için geçerli. System.String kullanımı yerine SecureString kullanımı tavsiye edilir.

  • SecureString bellekte şifreli tutulur (erişildiğinde şifresiz hale gelir),
  • GarbaceCollector bellekte gezinirken müdahale etmez, tek kopası bulunur,
  • IDisposable interface eklentisi var,
  • Pointer kullanımı mümkün (char array).

SecureString'in varlığı hassas bilgiyi string üzerinde tutmamanız için vardır. Hassas bilgi dışında kullanmamanız tavsiye edilir. Arka yapıda DPAPI kullanır.

Data Protection API (DPAPI)

  • DPAPI, şifreleme ile veri güvenliği sunar (şifre ve private key),
  • İşletim sistemi seviyesinde güvenlik sağlar yani başka kütüphanelere gereksinim duymaz,
  • Parola bazlı veri güvenliği sunan bir servistir. Güvenliğin sağlanması için parola gereklidir (giriş yapan kullanıcının parolası),
  • Crypt32.dll'in bir parçasıdır, tüm Windows işletim sistemlerinde bulunur,
  • DPAPI şifrelenmiş veriyi sizin için saklamaz, saklama işlemi için kendi depolama kodunuz olmalı,
  • DPAPI kullanıcının giriş bilgilerini kullanır,
  • Master Key adında güçlü bir anahtar (TripleDES ile) üretir. Bu anahtarı da üretirken kullanıcının parolasını kullanır. Bu anahtar da depolanmaz ve bir süre sonra kullanım ömrü dolar.
static void Main(string[] args)
{
	var str = ToSecureString(new[] { '1', '3', '5' });

	char[] charArray = CharacterData(str);

	// Baştaki orijinal verilere ulaşıyoruz
	string unsecureString = ConvertToString(str);

	Console.Read();
}

private static SecureString ToSecureString(char[] str)
{
	var secureString = new SecureString();

	Array.ForEach(str, secureString.AppendChar);

	return secureString;
}

private static char[] CharacterData(SecureString secureString)
{
	char[] bytes;
	var ptr = IntPtr.Zero;

	try
	{
		ptr = Marshal.SecureStringToBSTR(secureString);
		bytes = new char[secureString.Length];

		// Unmanaged bellekten char dizisine
		Marshal.Copy(ptr, bytes, 0, secureString.Length);
	}

	finally
	{
		if (ptr != IntPtr.Zero)
			// Unmanaged bellek temizleniyor
			Marshal.ZeroFreeBSTR(ptr);
	}

	return bytes;
}

// Hassas verinin tekrar string'e dönüştürülmesi önerilmez
// Bellekte birden fazla kopyası olması mümkün hale gelir
private static string ConvertToString(SecureString securePassowrd)
{
	var unmanagedString = IntPtr.Zero;

	try
	{
		// Unmanaged belleğe kopyalanıyor
		unmanagedString = Marshal.SecureStringToGlobalAllocUnicode(securePassowrd);

		return Marshal.PtrToStringUni(unmanagedString);
	}

	finally
	{
		Marshal.ZeroFreeGlobalAllocUnicode(unmanagedString);
	}
}

GÜNCELLEME
.NET Framework 4.7 ile gelen ve bu konuyu ilgilendiren bazı değişiklikler var

  • Improved support of RSA decryption with hardware keys,
  • Opening a cryptographic key with CspParameters.ParentWindowHandle set to this.Handle will now correctly make any PIN or password prompt be modal to the current window,
  • Enabled ClickOnce signing scenarios where certificate is identified by a cryptographic provider and private key container names.

Hangfire Hakkında

Bugünün konusu olarak belki duyduğunuz, belki de çoktan büyük ölçekli uygulamalarınızda kullandığınız Hangfire kütüphanesinden bahsedeceğim. Hadi başlayalım. (Github)

Hangfire Nedir?

Background job'ları (arka plan işleri) yaratmanıza, yürütmenize ve yönetmenize kolaylık sağlayan açık kaynaklı kütüphanedir. Job storage olarak bir çok veritabanı (SQL Server, SQL Server + MSMQ, Redis ve daha fazlası), IoC Container ve Unit Test desteklemektedir. Listesi için Hangfire Extension sayfasına göz atabilirsiniz. Hangfire Sidekiq, Resque ve Celery uygulamalarına .NET alternatifidir. 

Background Job Nedir?

Bazı kodların arka planda çalışması gerekmektedir. Çünkü bir iş parçacığının ana thread'de çalışması hem doğası gereği hem de bazı ihtiyaçlar dahilinde uygun olmayabiliyor.

Hangfire Kullanımının Artıları

  • Kullanımı kolay, bir kaç satırla tüm .NET uygulamalarınızda çalıştırabilirsiniz,
  • Yönetilebilirlik ve görünebilirlik sağlar,
  • İşler veritabanında tutulduğu için güvenilirdir. İş tamamlanmadıkça tamamlandı durumuna geçmez, kod bloğunun bitimine kadar çıkacak herhangi bir sorunda iş tekrar çalışacaktır,
  • Uygulamanızdan farklı, dağıtık şekilde kullanılabilir (Infrastructure),
  • ASP.NET uygulamalarında yaşanan sorunlara çözüm sağlar
    • Uzun süren request thread'ler,
    • Birden fazla yaratılmış background job instance'ı (aynı işin aynı zamanda yapılabiliyor olması sorunu),
    • IIS'in AppDomain ve App Pool recycle etmesi (Background job'ların yarım kalması ve tekrarlanmaması).

Hangfire'ın Desteklediği Background Job Tipleri

  • Fire and forget : Bir kere ve hemen çalışan background job tipi
var jobId = BackgroundJob.Enqueue(
    () => Console.WriteLine("Fire-and-forget!"));
  • Delayed : Bir kere fakat belirtilen sürenin sonunda çalışan background job tipi
var jobId = BackgroundJob.Schedule(
    () => Console.WriteLine("Delayed!"),
    TimeSpan.FromDays(7));
  • Recurring : Çok kez ve belirtilmiş CRON sürecinde (günlük, saatlik, haftalık veya CRON expressions vb.) çalışan background job tipi
RecurringJob.AddOrUpdate(
    () => Console.WriteLine("Recurring!"),
    Cron.Daily);
  • Continuations : Tanımlanan ana işin bitiminde çalışan background job tipi
BackgroundJob.ContinueWith(
    jobId,
    () => Console.WriteLine("Continuation!"));
  • Batch (PRO) : Birden fazla işin grup halinde çalışan background job tipi
var batchId = BatchJob.StartNew(x =>
{
    x.Enqueue(() => Console.WriteLine("Job 1"));
    x.Enqueue(() => Console.WriteLine("Job 2"));
});
  • Batch Continuations (PRO) : Grup halinde çalışan ana background job'ın bitimiyle çalışan background job tipi
BatchJob.ContinueWith(batchId, x =>
{
    x.Enqueue(() => Console.WriteLine("Last Job"));
});

NOT : PRO olarak ifade edilen background job tipleri Hangfire'ın ücretsiz sürümünde yer almamaktadır. Yıllık ücret karşılığında bu background job'lara erişilebilir. Ücretlendirme ile ilgili bilgilere ulaşmak için Hangfire Pricing ekranına göz atabilirsiniz. (Ek olarak : Compleks iş akışları, Redis depolama desteği, performance counter işlemleri)

Hangfire Architecture

Hangfire Kurulum ve Konfigürasyon

Hangfire'ı kullanmak istediğiniz projenize Nuget Package Manager yoluyla veya komutla Hangfire'ı yükleyin.

Hangfire NuGet Package

Komut : PM> Install-Package Hangfire (Nuget)

NOT : Farklı proje tipleri için farklı paketler yüklemeniz gerekebilir. Örneğin console uygulaması için Hangfire.Core yüklemeniz lazım fakat IIS'de host edilen web uygulaması için Owin de yüklenmelidir (dependency => Hangfire). Yükleme ve paket ile ilgili bilgiler için Hangfire Installation sayfasına göz atabilirsiniz.

Aşağıda paylaştığım konfigürasyon örneği (en basit haliyle), ASP.NET MVC projesidir.

using Owin;
using Microsoft.Owin;
using Hangfire;
using HangfireDemo;

[assembly: OwinStartup(typeof(Startup))]
namespace HangfireDemo
{
    public partial class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            // Hangfire sunucu bağlantı
            GlobalConfiguration.Configuration.
                UseSqlServerStorage(@"Server=.\SQLExpress;Database=HangfireDemo;Trusted_Connection=True;");

            // Hangfire dashboard kullan
            app.UseHangfireDashboard();

            // Hangfire sunucu kullan
            app.UseHangfireServer();
        }
    }
}

NOT : Batch kullanmak istiyorsanız ekstradan "GlobalConfiguration.Configuration.UseBatches()" satırını eklemeniz gerekiyor.

Hangfire yaratılan job'ları veritabanında tutar, tamamlandıkça da kendisi siler. Veritabanını kendisi yaratmıyor fakat gerekli tabloları kendisi oluşturuyor. (Uygulamanızın veritabanı ile Hangfire veritabanı farklı olabilir.)

Uygulamayı çalıştırdığınızda tabloların oluştuğunu göreceksiniz.

Hangfire veritabanı tabloları

Hangfire dashboard'a ulaşmak için uygulamanızın URL'inin sonuna "/hangfire" yazmanız yeterli.

Hangfire Dashboard

Hangfire Entegrasyonu

Hangfire'ı uygulamanızda kullanırken uygulayabileceğiniz bir kaç farklı senaryo var.

  • Single Process
    Hangfire Single Process
  • Web Garden
    Hangfire Web Garden
  • Web Farm
    Hangfire Web Farm
  • Separate Service
    Hangfire Separate Service
  • Separate Server
    Hangfire Separate Server

Hangfire Parametre İşlemleri

  • Çoklu parametre desteği var,
  • Parametreler serialise (JSON) edilip veritabanında tutuluyor (Array, collection, custom object),
  • Referans parametre ve ref ve out keyword'leri için desteği bulunmamaktadır,
  • Tüm kayıt yerine Id gibi belirgin değerin tutulması tavsiye edilir. Sebebi ise job storage'da tutulacak kaydın boyutu daha küçük olması.

Hangfire Dashboard

Host İşlemi

  • OWIN Middleware olarak yazılmıştır
    • ASP.NET, Nancy ve ServiceStack
  • OWIN Self Host
    • Console uygulaması ve Windows Service
  • Gereklilikler
    • Microsoft.Owin.Host.SystemWeb
    • OWIN Startup class
    • app.UseHangfireDashboard() konfigürasyon kodu

Sunduğu Özellikler

  • Servers (sunucular) : Kullanılan Hangfire sunucuları ve bilgilerini (name, workers, queues, başladığı an ve en son çalıştığı an vs.) gösterir
  • Recurring Jobs : Yaratılan recurring job bilgilerini gösterir. Bunlar : Cron, Time Zone, Job, ne zaman çalışacağı, en son ne zaman çalıştığı ve yaratıldığı tarih. Ayrıca, her ne kadar belirli bir tarih tanımlanmış olsa da Trigger özelliği ile istenilen an tetiklenebilir.
  • Retries (tekrar) : Yaratılan job'ların hata alması durumunda job'ların gösterildiği ekrandır. Kaç kere denendiği de görülebilir. Default olarak atanmış deneme (retry) sayısı 10'dur.
  • Jobs (işler)
    • Enqueued : Sırada olan işler,
    • Scheduled : İleri tarihe ayarlanmış işler,
    • Processing : Çalışan işler,
    • Succeeded : Başarılı şekilde tamamlanmış işler,
    • Failed : Başarısız olmuş işler (tanınmış atama sayısından sonra bile hata alınıyorsa iş buraya düşer),
    • Deleted : Silinmiş işler,
    • Awaiting : Sırasını bekleyen (continuations) işler.

Hangfire Dashboard Gelişmiş Konfigürasyon Seçenekleri

Hangfire Dashboard URL değişikliği ve geri yönlendirme opsiyonu

            // Dashboard üzerinden "back to site" button
            var options = new DashboardOptions { AppPath = VirtualPathUtility.ToAbsolute("/Home/Index") };
            // Dashboard custom URL
            app.UseHangfireDashboard("/ApplicationHangfireDashboard", options);

Birden fazla Hangfire Dashboard (ve/veya farklı veritabanları) kullanımı

            var storage1 = new SqlServerStorage("HangfireDatabase1");
            var storage2 = new SqlServerStorage("HangfireDatabase2");

            app.UseHangfireDashboard("/Hangfire1", new DashboardOptions(), storage1);
            app.UseHangfireDashboard("/Hangfire2", new DashboardOptions(), storage2);

            // Hangfire sunucu kullan
            // app.UseHangfireServer();

Hangfire Dashboard Güvenlik

Hangfire Dashboard'a default; local olarak erişilebilir. Örneğin sunucuda IIS'de çalışan bir web uygulamanızın Hangfire Dashboard'una remote (uzaktan erişim) makineden erişemezsiniz. Fakat uzaktan erişim için yetki vermeniz mümkün. Bunun için (IAuthorizationFilter 2.0.0 ile silinecek) IDashboardAuthorizationFilter interface'ini kullanmanız gerekiyor.

        public void Configuration(IAppBuilder app)
        {
            GlobalConfiguration.Configuration.
                UseSqlServerStorage(@"Server=.\SQLExpress;Database=HangfireDemo;Trusted_Connection=True;");

            app.UseHangfireDashboard("/hangfire", new DashboardOptions()
            {
                Authorization = new[] { new HangfireAuthorizationFilter() }
            });

            // Methodları çağırma sırası önemlidir
            // Önce authentication sonra 
            // HangFireServer
            app.UseHangfireServer();
        }

        public class HangfireAuthorizationFilter : IDashboardAuthorizationFilter
        {
            public bool Authorize(DashboardContext context)
            {
                if (HttpContext.Current.User.IsInRole("RoleName"))
                    return true;

                return false;
            }
        }

Daha fazla bilgi için => http://docs.hangfire.io/en/latest/configuration/using-dashboard.html#configuring-authorization

Hangfire Exception

Hangfire, uygulamanızda alacağınız hataları (default tekrar deneme sayısı : 10) hata detayı ile birlikte gösterir. Hata aldığınız job, retries sekmesine düşer. Retry sayısını Global ve Method seviyesinde değiştirmek mümkün. Örneğin retry sayısını 1 yaparsanız 1. denemeden sonra tekrar hata alınması durumunda job Failed sekmesine düşecektir.
Global seviyede retry sayısını değiştirmek için aşağıdaki kodu kullanabilirsiniz.

GlobalJobFilters.Filters.Add(new AutomaticRetryAttribute() {  Attempts = 1});

Method seviyesinde değiştirmek isterseniz, methodunuza aşağıdaki attribute'u ekleyebilirsiniz.

[AutomaticRetry(Attempts = 1)]

Eğer fail olan joblarınızın manuel değil de otomatik olarak silinmesini istiyorsanız
AutomaticRetry(OnAttemptsExceeded = AttemptsExceededAction.Delete) kodunu kullanabilirsiniz.
Daha fazla bilgi için => http://docs.hangfire.io/en/latest/background-processing/dealing-with-exceptions.html

HangFire Logging

Hangfire otomatik loglama desteğine (Serilog, NLog, Log4Net, EntLib Logging, Loupe ve Elmah) sahiptir. Otomatik loglama; startup sınıfında log provider tanımlamanızın yettiği anlamına geliyor. Ayrıca custom loglama desteği (ILogProvider, ILog) de mevcuttur.
Daha fazla bilgi için => http://docs.hangfire.io/en/latest/configuration/configuring-logging.html

HangFire SQL Server Ayarları

            var serverOptions = new SqlServerStorageOptions
            {
                // Hangfire'ın ne kadar süre aralıkta kontrol edeceği bilgisi
                // Default değeri 15 saniye
                QueuePollInterval = TimeSpan.FromSeconds(45),
                // Veritabanında tabloların yaratılıp yaratılmayacağı bilgisi
                // Manuel migration işlemleri için false yapılabilir
                // Default değeri true
                PrepareSchemaIfNecessary = false
            };

            GlobalConfiguration.Configuration.UseSqlServerStorage("Veritabanı", serverOptions);

Daha fazla bilgi için => http://docs.hangfire.io/en/latest/configuration/using-sql-server.html

NOT : Hangfire, Redis desteklemektedir. Redis'in tercih sebepleri arasında yüksek performans (in-memory) ilk sıradadır fakat Redis kullanmak için Hangfire PRO yani ücretli versiyonu kullanılması zorunludur. Daha fazla bilgi için => http://docs.hangfire.io/en/latest/configuration/using-redis.html

Hangfire Performance Comparison

Ve son olarak dikkat edilmesi gerekenler...

  • Job'ları kullandığınız metotlar yarıda kesilebilir ve tekrarlanabilir,
  • Methodlarda kullandığınız parametreler, kompleks olmayan (basit), küçük ve object yerine primitif tip,
  • Unit test ve IoC'ye uygun,
  • Job takibi Polling vs. Pushing (örnek : SignalR) ayrımı gözetilerek

yazılırsa kodunuz optimize olmuş olur.

DataTable'da Aggregate Fonksiyon Kullanma (Compute Metodu)

ADO.NET'in sunmuş olduğu başka bir güzellikle karşınızdayım. Bu sefer DataTable üzerinden Aggregate Function'ları kullanacağız. Tam olarak hangi işlemleri, nasıl ve neden yapalım? Şimdi öğreneceksiniz.

Senaryo : DataTable nesnesinin Compute metodunda kullanabileceğimiz aggregate fonksiyonları, Northwind veritabanının Products tablosundaki UnitsInStock ve UnitPrice kolonları üzerinde örneklendireceğim.

Bilinmesi Gerekenler :

  • Makaledeki örnek uygulama .NET Framework 4.0 (Windows Form) ve Visual Studio 2010 ile hazırlandı,
  • Makaledeki örnek uygulamada DataTable'ı doldurmak için Northwind'i kullandım. Eğer Northwind veritabanına sahip değilseniz, örnek uygulamanın çalışması için buradan indirebilirsiniz. Northwind veritabanını kullanma sebebim genelde yazılımcılarda mevcut olması. Projeye eklememe sebebim ise indireceğiniz örnek uygulamanın boyutunu küçük tutmak.

Neler Öğreneceksiniz :

  • DataTable nesnesinin Compute metodunu nasıl ve neden kullanmanız gerektiğini.

İlk olarak DataTable'ın Compute metoduyla hangi aggregate fonksiyonları kullanabiliyoruz ona bakalım.

  1. Sum - Toplam değer,
  2. Avg - Ortalama değer,
  3. Min - En küçük değer,
  4. Max - En büyük değer,
  5. Count - Kayıt sayısı,
  6. Var - Varyans değer,
  7. Stdev - Standart sapma değer.

Kullanımına gelirsek, bilmeniz gereken tek şey metot 2 tane parametre alıyor:

public object Compute(string expression, string filter);

İlk parametre kullanmak istediğiniz fonksiyonu, ikincisi ise filtre tanımı. Kullanmak istediğiniz fonksiyonun SQL'deki kullanımından hiçbir farkı yok. Filtre dediğim kısım ise "CategoryID='2'" yazdığımızda ID'si 2 olan verileri getirecektir. Örnek vereyim :

SqlDataAdapter adapter = new SqlDataAdapter("select UnitPrice,CategoryID from Products", "server=.; database=northwind; integrated security=sspi");

DataTable table = new DataTable();

adapter.Fill(table);

string result = table.Compute("SUM(UnitPrice)","CategoryID='2'").ToString();
MessageBox.Show(result);

adapter.Dispose();
table.Dispose();

Yukarıdaki örnekte Products tablosundan UnitPrice ve CategoryID verilerini çektik. Daha sonra CategoryID'si 2 olan verilerin UnitPrice'larını topladık. SQL'e sorgu yazmaktan bir farkı yok ve eğer denemek isterseniz aşağıdaki sonuçla karşılaşıcaksınız. Eğer Northwind veritabanında oynama yapmadıysanız :)

Sonuç

Eğer filtre uygulamak istemiyorsanız yapmanız gereken tek şey metodun ikinci parametresini boş bırakmak. Hepsi bu.

Gelelim ne gibi yerlerde kullanabiliriz? Eğer veritabanının bir tablosunu sayfanın/uygulamanın açılış anında dolduruyorsanız ve bu tablo üzerinden işlem yapıyorsanız bir kereliğe mahsus DataTable nesnesi üzerinde tutarsınız (Sınıf düzeyinde tanımlanmış bir DataTable) bu DataTable nesnesi üzerinden işlemleri gerçekleştirebilirsiniz. Böylece veritabanı ile bağlantı kurmaktan da kurtulursunuz.

Not :

1 - Eğer bilinmeyen bir aggregate fonksiyonu kullanırsanız aşağıdaki gibi hata alırsınız.

Hata

The expression contains undefined call X(). Hatanın Türkçesi : "İfade tanımlanmamış bir işlev çağrısı içeriyor: X()"

2 - Eğer mantıksal hata yaparsanız mesela ProductName kolonu üzerinde SUM gibi toplama fonksiyonu kullanmak isterseniz, hata alırsınız. Sebebi veri tipinin string olması ve toplayamaması. Alacağınız hata da şöyle olacaktır.

Hata

Invalid usage of aggregate function Sum() and Type: String. Hatanın Türkçesi : "Geçersiz Sum() toplam fonksiyonu ve Tür kullanımı: String."

İçerisinde tüm işlemleri barındıran ve filtrenin opsiyonel olduğu örnek uygulamamı indirmek isterseniz buraya tıklayın.

ADO.NET Bağlantı Havuzu

Herhangi bir veritabanı ile ilgili bir işlem için açılan bağlantı, sunucu tarafında oturum açılmasına sebep olur. Açılan her bağlantı yeni bir istek, her istek yük anlamına gelir. Birazdan okuyacağınız makalede tüm isteklerin bir bağlantıdan gönderilmesi (connection pooling - bağlantı havuzu) ile her isteğin ayrı ayrı bağlantılar üzerinden gönderilmesi arasındaki farkı ve avantajları göreceksiniz.

Senaryo : Veritabanına, içerisinde pooling özelliği true ve false olan 2 farklı bağlantı açacağım. Bize ne kadarlık süreye mal olacağını ve SQL Server Profiler'da nasıl görüneceğini göstereceğim.

Bilinmesi Gerekenler :

  • Makaledeki örnek .NET Framework 4.0 (Windows Form) ve Visual Studio 2010 ile hazırlandı,
  • Makalede Northwind veritabanı üzerinden bağlantı açacağım. Eğer Northwind veritabanına sahip değilseniz hata verecektir. İndirmek için buraya tıklayın. Northwind veritabanı kullanmamın sebebi çoğu yazılımcının mevcut olması. Projenin boyutunu büyütmemek adına veritabanını projeme eklemedim.

Neler Öğreneceksiniz :

  • Tanımlanan bağlantı üzerindeki pooling (havuz) mantığını kavrayacaksınız,
  • Min Pool Size ve Max Pool Size kavramlarını öğreneceksiniz.

Önceklikle şunu belirtmeliyim ki eğer tanımladığınız bağlantı cümlesine "pooling" özelliğini eklemezseniz, varsayılan olarak true olacaktır. Yani oluşturulmuş bağlantı havuzu üzerinden işlemlerinizi gerçekleştirecekseniz bir şey yapmanıza gerek yok. Ancak her istek için ayrı bir bağlantı tanımlamak istiyorsanız aşağıdaki gibi bağlantı tanımlayabilirsiniz.

SqlConnection connection = new SqlConnection("server=.; database=northwind; integrated security=sspi; pooling=false");

İlk olarak veritabanı ile bağlan kuralım ve SQL Server Profiler'dan veritabanında neler olduğuna bakalım.

// 1 numaralı bağlantım.
using (SqlConnection connection = new SqlConnection("server=.; database=northwind; integrated security=sspi;"))
{
  connection.Open();
}

// 2 numaralı bağlantım.
using (SqlConnection connection = new SqlConnection("server=.; database=ReportServer; integrated security=sspi;"))
{
  connection.Open();
}

// 1 numaralı bağlantım ile aynı olduğu için 3. bağlantıyı açmayıp, 1 numaralı bağlantı üzerinden işlemi gerçekleştirecek.
using (SqlConnection connection = new SqlConnection("server=.; database=northwind; integrated security=sspi;"))
{
  connection.Open();
}

Ve sonuç aşağıdaki gibi olacaktır. 

SQL Server Profiler

Gördüğünüz gibi tek bir havuzdan yönetilen bağlantılar sonucu, aynı bağlantılari çin tekrar tekrar oturum açılmadı. Gelin şimdi her istek için ayrı bir bağlantı açalım ve bu işlemin ne kadar süreceğine bakalım.

SqlConnection connection;

DateTime start = DateTime.Now;

// 1000 kere bağlantıyı açıp kapatacağız.
for (int i = 0; i < 1000; i++)
{
  connection = new SqlConnection("server=.; database=northwind; integrated security=sspi; pooling=false");

  connection.Open();
  connection.Close();
  connection.Dispose();
}

DateTime end = DateTime.Now;

// Geçen zamanın sonucunu alacağız.
TimeSpan result = end - start;

MessageBox.Show(String.Format("Açılan bağlantıların işlem süresi : {0}", result.ToString()));

Yukarıdaki işlemin sonucu tam olarak 00:00:07.3951136 saniye sürdü. Anlattığım kadarıyla "pooling=false" dediğimiz için bin ayrı oturum açılmış olması lazım. 

SQL Server Profiler

Evet liste böylece akıp gidiyor aşağıya doğru. Şimdi... Şimdi "pooling=true" yapacağız. Yani tek bir bağlantıyı açık tutacağız ve işlemleri bellekteki bağlantıdan yürüteceğiz. Gelin bunun ne kadar zaman alacağına ve SQL Server Profiler'da nasıl gözükeceğine bakalım.

SqlConnection connection;

DateTime start = DateTime.Now;

for (int i = 0; i < 1000; i++)
{
  connection = new SqlConnection("server=.; database=northwind; integrated security=sspi; pooling=true");

  connection.Open();
  connection.Close();
  connection.Dispose();
}

DateTime end = DateTime.Now;

TimeSpan result = end - start;

MessageBox.Show(String.Format("Açılan bağlantıların işlem süresi : {0}", result.ToString()));

Yukarıdaki işlemin sonucu tam olarak 00:00:00.2275035 saniye sürdü. Buradaki asıl mantık şu; açılan ilk bağlantı bellekte tutulur ve geri kalan bağlantılar için kullanılır. Gelin bir de SQL Server Profiler'da nasıl göründüğüne bakalım.

SQL Server Profiler

Dediğim gibi, tek bir bağlantı açıldı ve geri kalan bağlantılar bellekten kullanıldı. Gördüğünüz gibi inanılmaz performans artışı sağlandı. Peki bağlantı havuzundaki bağlantı sayımıza sınır koyma şansımız var mı? Evet.

Max Pool Size : Bağlantı havuzumuzdaki saklanacak en fazla bağlantı sayısını belirtir. Varsayılan olarak 100'dür.

Min Pool Size : Bağlantı havuzumuzdaki saklanacak en az bağlantı sayısını belirtir. Varsayılan olarak 0'dır.

Eğer Max Pool Size'ı 50 olarak belirtir ve 50'den fazla bağlantı açarsanız (kapatmadan), şöyle bir hata ile karşılacaksınız.

Hata

"Timeout expired.  The timeout period elapsed prior to obtaining a connection from the pool.  This may have occurred because all pooled connections were in use and max pool size was reached." Diyor ki : Zaman aşımı süresi doldu. Havuza bağlantı elde edilemeden zaman aşımı süresi doldu. Bu, tüm havuz bağlantıları kullanıldığı ve en büyük havuz boyutuna erişildiği için oluşmuş olabilir. Bağlantınızı kapatmayı asla unutmayın! (Bağlantılı sınıflarda tabii)

Son olarak SqlConnection üzerinden kullanabileceğiniz 2 adet statik metottan bahsetmek istiyorum.

  1. SqlConnection.ClearAllPools() ile tüm havuzları temizleyebilir,
  2. SqlConnection.ClearPool(SqlConnection) ile belirli bağlantı havuzunu temizleyebilirsiniz.

Makaledeki örnek uygulamayı indirmek isterseniz buraya tıklayın.