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

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.

DataTableReader Nesnesi

Bu makalemizde DataTableReader nesnesinin ne olduğunu ve nasıl kullanılacağını öğreneceğiz.

Senaryo : SqlDataAdapter nesnesi ile verileri veritabanından çekip bir DataTable'ı dolduracağım. Daha sonra DataTableReader ile bu DataTable üzerinden verileri okuyacağım.

Bilinmesi Gerekenler :

  • Makaledeki örnek .NET Framework 4.0 ve Visual Studio 2010 ile hazırlandı,
  • Makale, Northwind veritabanı gerektirmektedir. Northwind veritabanını indirmek istiyorsanız 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 :

  • DataTableReader nesnesinin ne olduğunu, nerede ve nasıl kullanacağımızı öğreneceğiz.

DataTableReader ilk olarak aklınıza SqlDataReader'ı getirebilir ki haksız değildir. Çok benzer yapıya sahiptirler. DataTableReader da SqlDataReader gibi DbDataReader sınıfından türemiştir. İkisi de read-only ve forward-only yapıya sahiptir. Madem ki DataTableReader ile SqlDataReader bir çok ortak noktaya sahip, farkı neresinde? SQL veritabanı için nasıl ki SqlDataReader kullanılıyor, DataTable veya DataSet için de DataTableReader kullanılıyor ve bağlantının açık kalması gibi bir durum söz konusu değil. Çünkü bağlantısız nesneler (disconnected mimari) üzerinden çalışıyor. 

SqlDataAdapter adapter = new SqlDataAdapter("Select FirstName,LastName from Employees", "server=.; database=northwind; integrated security=sspi");

DataTable table = new DataTable();

adapter.Fill(table);

//DataTableReader bu satırda yaratılıyor.
DataTableReader tableReader = table.CreateDataReader();

if (tableReader.HasRows)
{
  while (tableReader.Read())
  {
     lst_employees.Items.Add(String.Format("{0} {1}", tableReader[0], tableReader[1]));
  }
}

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

DataSet nesnesi üzerinden DataTableReader yaratmak için:

DataSet dataSet = new DataSet();

adapter.Fill(dataSet);

DataTableReader tableReader = dataSet.CreateDataReader();

Birden fazla DataTable üzerinde çalışmak için NextResult metodunu kullanabilirsiniz. Dediğim gibi, SqlDataReader'dan pek farkı yok ama kullanım alanında farklılık yaratabilir.

Makale ile ilgili örnek uygulamayı indirmek isterseniz buraya tıklayın.

Tutarlılık İhlali Kontrolü

Tutarlılık ihlali kavramı (Eşzamanlılık ihlali), birden fazla kişinin aynı veri üzerinde aynı anda değişiklik yapması sonucu ortaya çıkan sorundur. Çok kullanıcılı sistemlerde ortaya çıkması zor ancak çıktığı zaman hataya sebebiyet veren bir sorundur. Aşağıdaki makalede bu soruna çözüm üretip, uygulayacağız.

Senaryo : Veritabanındaki bir kayıt üzerinde değişiklik yaparak DBConcurrencyException hatası alacağız ve bunun çözümünü uygulayacağız.

Bilinmesi Gerekenler :

  • Makaledeki örnek .NET Framework 4.0 ve Visual Studio 2010 ile hazırlandı,
  • Makale, Northwind veritabanı gerektirmektedir. Northwind veritabanını indirmek istiyorsanız buraya tıklayın. Northwind veritabanı kullanmamın sebebi çoğu yazılımcının bilgisayarında mevcut olması. (projenin boyutunu artırmamak için eklemedim)

Neler Öğreneceksiniz :

  • Aşağıdaki makalenin sonucunda Optimistic Locking ve Pessimistic Locking ifadeleri anlayacaksınız,
  • DBConcurrencyException hatası ile karşılaştığınızda ne yapacağınızı bileceksiniz.

Öncelikle yaşadığımız/yaşayacağımız sorunu anlayalım. Çoklu kullanıcının kullanıldığı bir veritabanındaki bir satırı çeken Kullanıcı1 adlı kişi ve ardından aynı satırı çeken Kullanıcı2'yi düşünün. Kullanıcı1 satırda değişiklik yapıp veritabanına güncellediği zaman bundan Kullanıcı2'nin haberi olmayacaktır ve Kullanıcı2 de satırı değiştirmek istediği zaman hangi verinin tutarlı olup olmayacağı gibi bir sıkıntı oluşuyor. Bu sıkıntıyı önlemek adına iki tane yöntem var. Bunlar:

Optimistic Locking (İyimser Kilitleme) / Optimistic Concurrency Control (İyimser Tutarlılık İhlali Kontrolü) : Aynı anda birden fazla kişinin aynı veri üzerinde değişiklik yapabilme prensibine dayalıdır. Çünkü aynı anda birden fazla kişinin aynı veri üzerinde oynama ihtimali düşüktür. İşlem sırasında kilit konmaz, kullanıcı tarafından işlem yapılırken değişiklik olup olmadığına dair kontrol edilir.

Pessimistic Locking (Kötümser Kilitleme) / Pessimistic Concurrency Control (Kötümsel Tutarlılık İhlali Kontrolü) : Bu kontrolde, ilk kullanıcı veriyi okuduğu an veri üzerine kilit konur. Böylece sonraki kullanıcıların veriye erişmesi engellenerek, verinin doğruluğu garanti altına alınmış olur.

Örnekten önce Optimistic Locking ile ilgili bilgi vermek istiyorum. Optimistic Locking'de, değişiklik yapılacak verinin üzerine kilit konulmadığı için bir kaç kontrol yapılmalıdır. Bunlardan ilki değişiklik yapılacak verideki kolonun ilk hali kontrol edilmelidir. Örnek :

UPDATE Employees set LastName = @YeniLastName where LastName = @OrjinalLastName

İkinci yol ise satırın üzerinde bulunan timestamp kolonunun değişip değişmediği kontrol edilir. (timestamp read-only olup mevcut satırın versiyonunu üzerinde tutan veri tipidir. Satır üzerinde yapılan değişiklikte timestamp değeri de değişir.)

UPDATE Employees set LastName = @YeniLastName where TimeStampID = @OrjinalTimeStampID

Yukarıdaki yöntemler sırasında eğer hata meydana gelirse bu da DBConcurrencyException olacaktır. Şimdi hata alabileceğiniz bir uygulama yapalım. Senaryomuz : Veritabanındaki bir satırda güncelleme yapmadan önce programı bir yerde durdurup, veritabanından elle değiştirdikten sonra programımızdan güncelleme işlemine devam edeceğiz. Yani sistemde veri tutarsızlığına sebebiyet vecerek bir durum yaratacağız.

SqlDataAdapter adapter = new SqlDataAdapter();
SqlConnection connection = new SqlConnection("server=.; database=northwind; integrated security=sspi");
SqlCommand command = new SqlCommand("select * from employees", connection);

// SqlDataAdapter'e select komutumuzu veriyoruz.
adapter.SelectCommand = command;

command = new SqlCommand("update employees set LastName=@LastName where FirstName=@FirstName", connection);
command.Parameters.AddWithValue("@LastName", "Esmer");
command.Parameters.AddWithValue("@FirstName", "Firat");

// SqlDataAdapter'e update komutumuzu veriyoruz.
adapter.UpdateCommand = command;

DataSet set = new DataSet();
adapter.Fill(set, "Employees");

// SqlDataAdapter - Update işlemini yapabilmek için herhangi bir değişiklik yapıyoruz.
DataTable table = set.Tables[0];
table.Rows[0]["LastName"] = "DenemeSoyad";

/* Bu satıra breakpoint koyup veritabanınızdan, update sorgumuzda where koşulunda belirtmiş olduğumuz veriyi 
değiştirmenizi istiyorum. (yani FirstName'i Firat yerine başka bir şey yazın.)*/
int rowCount = adapter.Update(set, "Employees");

adapter.Dispose();
connection.Dispose();
set.Dispose();

DBConcurrencyException

Breakpoint'i koyduğunuz yerde veritabanınızdan veriyi değiştirip Update metodunu çalıştırdığınızda GÜM! "Concurrency violation: the UpdateCommand affected 0 of the expected 1 records." hatası alıyoruz. Türkçe : "Tutarlılık ihlali: UpdateCommand, beklenen 1 kaydın 0 kaydını etkiledi." Buradaki hatayı almamızın sebebi UpdateCommand'da belirtilen FirstName'in ilk başta Firat, daha sonra veritabanından değiştirdiğimiz şekilde değişmesi ve buna bağlı tutarsızlığın meydana gelmesi. Bu hatayı gidermek için SqlDataDapter nesnesinin RowUpdated olayını (event'ini) kullanabiliriz. Az önce hata aldığımız Update metotu satırında RowUpdated olayı tetiklenecek. RowUpdated olayı içerisinde DataRow nesnesinin HasErrors özelliğini kullanarak hatalı satır olup olmayacağını kontrol edeceğiz ve eğer satırda hata varsa satır işlenmeyecek, es geçilecek.

SqlDataAdapter adapter = new SqlDataAdapter();
SqlConnection connection = new SqlConnection("server=.; database=northwind; integrated security=sspi");
SqlCommand command = new SqlCommand("select * from employees", connection);

adapter.SelectCommand = command;

command = new SqlCommand("update employees set LastName=@LastName where FirstName=@FirstName", connection);
command.Parameters.AddWithValue("@LastName", "Esmer");
command.Parameters.AddWithValue("@FirstName", "Firat");

adapter.UpdateCommand = command;

adapter.RowUpdated += adapter_RowUpdated;

DataSet set = new DataSet();
adapter.Fill(set, "Employees");

DataTable table = set.Tables[0];
table.Rows[0]["LastName"] = "DenemeSoyad";

/* Bu satıra breakpoint koyup veritabanınızdan, update sorgumuzda where koşulunda belirtmiş olduğumuz veriyi değiştirmenizi 
istiyorum. (yani FirstName'i Firat yerine başka bir şey yazın.)*/
int rowCount = adapter.Update(set, "Employees");

string hataliSatir = string.Empty;

foreach (DataRow item in table.Rows)
{
   if (item.HasErrors)
    {
      // Değişiklik yapılacak satırın sırası ve hatasını alıyoruz.
      hataliSatir = String.Format("{0} -> {1}", item[0], item.RowError);
    }
}

adapter.Dispose();
connection.Dispose();
set.Dispose();

// İşlemin en sonunda kullanıcıyı bilgilendirecek mesajı gösteriyoruz.
MessageBox.Show(String.Format("Güncellenen kayıt sayısı : {0}\nHatalı Satır : {1}", rowCount, hataliSatir));
}

/* SqlDataAdapter satır güncelleme olayı. Hatayı burada yakalayacağız ve hata mesajını belirtip kullanıcıyı 
bilgilendireceğiz.*/
void adapter_RowUpdated(object sender, SqlRowUpdatedEventArgs e)
{
    // 0 = Değişiklik olmadığını gösterir.
    if (e.RecordsAffected == 0)
    {
      e.Row.RowError = "Tutarlılık ihlali: UpdateCommand, beklenen 1 kaydın 0 kaydını etkiledi.";

      // Hatalı satır üzerinde işlem yapılmadan es geçiliyor.
      e.Status = UpdateStatus.SkipCurrentRow;
     }
}

Evet gördüğünüz gibi kilitleme yapmadık, ancak hata da almadık. Pessimistic Locking pek kullanmıyorum. Onun yerine Optimistic Locking ile hata kontrolü yapıyorum. Eğer kilitleme yapmak istiyorsanız (satır) aşağıdaki basit yapıya göz atın derim.

SELECT * FROM Employees  (HOLDLOCK, ROWLOCK) WHERE  EmployeeID = 10

Eğer hazırlamış olduğum örnek uygulamayı indirmek isterseniz buraya tıklayın.