Uzun zamandır bir şeyler yazamıyordum maalesef. Bu sebepten dolayı 23 Nisan tatilini fırsat bilip hemen bir şeyler karalamak istedim bende :) Bu arada yeri gelmişken tüm Dünya çocuklarının 23 Nisan’ı kutlu olsun. Atatürk’ün bu bayramı sadece Türk olan çokcuklara değil de tüm Dünya’daki çocuklara armağan ettiğini de unutmamak lazım bence. Konumuza dönersek eğer, genelde hepimiz mutlaka bir gramlama diline başladıktan bir süre sonra bir veritabanı programı yazarız yada yazmaya çalışırız J veya veriler üzerinde işlemler yapan benzer nitelikte küçük programlar geliştiririz. Kimimiz basit bir telefon defteri, kimimiz bir stok takip programı yazar. Arada kendi muhasebe programını yazıp çok para kazanmayı düşünenler bile vardır mutlaka J ki kullandıgınız programlama dili C# ise bu gibi şeylere çok daha yakınsınızdır. Çünkü .net 3.0 ile gelen LINQ yapısının tek hedefi veri sorgulama olduğu için Microsoft’un bu alanı ne kadar önemsediğini çok daha net görebiliriz.
Peki bu veritabanı üzerinde işlem yapan programlar C# ile nasıl geliştiriliyor? Bildiğiniz gibi C# ta veriye erişim ADO.NET dediğimiz teknoloji ile sağlanıyor. Peki nedir bu ADO.NET? Burada konumuz ADO.NET mimarisi olmadığı için derin bir analiz yerine ADO.NET ‘in sadece belli başlı sınıflarına değineceğim ki konunun ilerleyen kısımlarında bunlar bize gerekli olacak. Ayrıca yeri gelmişken hemen söyliyeyim, ADO.NET için Aykut Taşdelen ‘in C# ile Veritabanı Programlama ve ADO.NET kitabını aynı zamanda www.csharpnedir.com ‘da ADO.NET başlığı altında Burak Selim Şenyurt ‘un yazdığı makaleleri kesinlikle okunmanızı tavsiye ederim. Eğer bu konularda bir sıkıntınız yoksa yazıyı okumaya devam edebilirsiniz ama C# ‘a yeni başlayan biri iseniz öncelikle veri tabanı programcılığı hakkında biraz bilgi edinmelisiniz.
Bildiğiniz gibi ADO.NET mimarisi ile bizler veritabanına bağlanıp, SQL sorgusu ile bir tablodan istediğmiz kayıtları seçip, bunları bir DataGrid kontrolünde listeleyebiliyor, yeni bir kayıt ekleyeceğimiz zaman bilgileri TextBox’lara girip kaydet dediğimizde de eklediklerimiz veritabanına kayıt ediliyordu. Peki bu işlemleri ADO.NET’ te hangi sınıflar yapıyordu ? (Örnek SQL Server üzerinde yapılacagı için kullanacagımız provider SqlClient olacaktır)
Öncelikle hangi server daki, hangi veritabanına hangi şekillerde bağlanacagımızı belirten bir connectionString yani bağlantı cümlesi yazmamız gerekiyor.
Ardından bir connection nesnesi yaratlıyordu ve buna yazdıgımız baglantı cümlesini parametre olarak veriyorduk.
Connection nesnesinin open methodu ile baglantı cümlesindeki veritabanına bağlanılıyordu.
Tüm kayıtları sececeğimiz zaman ise bir Sql SELECT sorgusu yazıyorduk ve bunuda bir DataAdapter nesnesi’nin Fill methodu ile bir DataTable nesnesine dolduruyorduk.
Sonra bu DataTable nesnesini bir GridView ‘e Data Source olarak gösteriyorduk ve sorgumuzun geriye döndürdüğü kayıtları artık uygulamız içindeki bir DataGridde görebiliyorduk.
Bu adımlar da bir tablo içeriği gösterilmiş oluyordu.
Peki yeni bir kayıt ekleyeceğimiz zaman ne yapıyorduk.
Bir Command nesnesi tanımlıyorduk, bu nesne bizim en başta tanımladıgımız Connection nesnesi ile ilişkili çalışıyordu.Bunu command nesnesinin connection özelliği sayesinde belirtiyorduk.
Eğer sorgumuz parametre alıyorsa bunuda Command nesnemizin parameters koleksiyonuna ekliyorduk.
Sonra CommandText özelliğine verdiğimiz Sql komutunu, bağlantıyı açtıktan sonra ExecuteNonQuery() methodu ile çalıştırabiliyorduk.
Ayrıca bu Command nesnesi aynı zamanda bir veritabanı nesnesi olan ve veritabanı üzerinde saklanan Stored Procedure’ leride çalıştırabiliyordu. Ve Command nesnesi aynı zamanda ExecuteScalar() methodu ile geriye tekil sonuclar döndüren Sql sorgularıda çalıştırabiliyordu. Mesela bir tablodaki kayıt sayısı gibi.
Buraya kadar maddeler halinde yazdıgım satırları okurken belki sıkılmışsınızdır ama sıkıcı bile olsa bunlar C# dilinde, ADO.NET mimaris ile en basit haliyle bir veritabanı üzerinde işlem yapmanın başlıca adımlarıdır. İşte bugünkü makalemin konusu sizi bu sıkıcı adımları sürekli gerçekleştirmekten kurtarmak :)
Bunuda Data Access Layer dediğimiz bir veri erişim katmanı sayesinde yapıyoruz. Adı sizi çok korkutmasın sakın J Aslında arka planda çalışan mantık yukarda yazdığım adımlardan pekte farklı değil. Peki nedir bu Data Access Layer (DAL) ? DAL, kısaca veritabanı üzerinde işlem yapan bir veya birkaç class’ın yazılıp, paketlenip bir .dll dosyası haline getirilmiş halidir diyebiliriz. Bu işlem bir nevi bizi veritabanından soyutlamış olur. Biz veri üzerinde bir işlem yaparken ADO.NET sınıflarına direk erişmek yerine bunlara DAL içindeki methodlar ile erişip işlemlerimizi yaptırıyoruz. Bildiğiniz gibi .dll dosyalarını projelerimize add referance ile ekleyip içindeki sınıfları kullanabiliyoruz. Bu sayede o sınıftaki kodları sürekli projemiz içinde yazmaktansa bir kez bir .dll dosyasına yazıp bunu her projede kullanabiliyoruz. İşte bu mantıktan yola çıkarak bizde ufak bir Data Access Layer geliştireceğiz. Amacım sadece size işin mantığını sunabilmek oldugu için cok karmaşık yapılara girmeyeceğim. Bu mantıgı kavradıkta sonra eminim çok zorlanmadan kendi DAL’ınız yazabileceksiniz. Ayrıca daha detaylı bir DAL yazımı için sevgili Levent Yıldız ‘ın linkteki makelesini inceleyebilirsiniz.
Bu ön bilgilerden sonra projemize başlama zamanı geldi sanırım. Şimdi kendimize bir şablon çıkarmamız gerekiyor. Aslında şablonu az önce yukardaki adımlar ile çıkarmıştık. Şimdi ondan yararlaranarak bu şablonun detaylarıda kapsayan bir yapı geliştirmek.
Bu yapıyı aşağıdaki grafikte özetlemeye çalıştım.

Data Access Layer adında bir ClassLibrary’miz var. Yani bu bizim NameSpace’imiz oluyor. Bunun içinde DataBase adında bir Class var. Ve onun içindede veritabanı nesnelerimiz, property’ lerimiz, SQL ve Stored Procedure sorgularını çalıştıracak methodlarımız ve son olarakta bu sorgulara gidecek olan parametreleri belirleyen yada temizleyen Parametre methodları var. Yapıyı anlaşılır olması amacıyla oldukça basit tutmaya çalıştım. Daha profosyonel bir şey yapmak isterseniz mesela Parameters bölümünü ayrı bir class olarak yazabilirsiniz.
DataBase bölümünde bağlantı cümlesine göre bir Connection nesnesi oluşturan 3 farklı method var. Bunlardan iki tanesi CreateDataBase adından parametre alan ve almayan methodlar. Diğeri ise Default Constructor’ ın overload edilmiş hali olan connection stringi direk parametre alarak bir Connection nesnesi oluşturan Constructor’ dır. Bunun bize kolaylıgı ilerde bu DataBase sınıfından bir nesne yaratırken aynı anda Connection nesnemizi oluşturabilmemizdir.
Objects bölümü bizim veritabanı işlemleri yapmamızı sağlayacak ADO.NET sınıflarını içeriyor.
Properties bölümü ise connection stringimizi dinamik bir yapı halinde sunabilmek için alacagı parametrelere değer atamayı sağlayacak özellikleri içeriyor.
Static SQL bölümünde ise Command nesnesi yardımı ile C# kodu içine gömülü SQL kodu çalıştırabilmek için geçerli olan methodları içeriyor. Bunlardan RunSqlTable bir select sorgusunu geriye bir DataTable şeklinde döndürüyor. RunSqlQuery ise Insert, Update, Delete gibi geriye değer döndürmesi şart olmayan SqlKodlarını çalıştırıyor(ExecuteNonQuery). Gerekirse sorgunun başarılı olup olmadıgının kontrolü için tabiki bool döndüren bir yapıyada sahip olabilir. RunSqlScalar ise geriye tekil sonuç döndüren Sql sorgularının çalıştırılması için çağrılır ve geriye sorgunun döndürdüğü degeri object olarak döndürür. Bizde bunu cast ederek istediğimiz tipe cevirebiliriz. Bu methodların hepsi SQL sorgusunu bir string parametre olarak alıyor.
Stored Procedure altında ise Static Sql ‘deki methodların Stored Procedure şeklinde çalışan versiyonları vardır. RunSPTable, RunSPQuery, RunSPScalar bu 3 method parametre olarak ise Stored Procedure’ ün ismini alır.
Son olarak ise Parameters bölümü vardır. Burada ise sorgularımıza parametre göndermek istersek AddParameter ile parametre ekliyoruz. Yada yeni bir sorguda, farklı isimde parametreler kullanacaksan önceden eklenen parametreleri ClearParameters methodu ile siliyoruz.
Dediğim gibi mantığın anlaşılması için çok basit bir yapı kurmaya çalıştım. Umarım karışık gelmez. Şimdi isterseniz uygulamamızı geliştirmeye başlayalım.
İlk olarak Visual Studio 2005 ‘i açıyoruz ve projemiz bir .dll dosyası olacagından vede diğer projelerde kullanacağımızdan dolayı projemizi bir ClassLibrary olarak açıyoruz. Adınada kısaca DAL diyoruz.

Sınıfımızın adınıda DataBase olarak belirleyelim. Ardından using deyimi ile System.Data ve System.Data.SqlClient namespace’ lerini ekleyelim.
Şimdi yukarda verdiğim tabloya göre projemizi geliştirmeye başlayalım. Sırayla gittiğimize göre ilk olarak nesnelerimizi ve property’lerimiz bir bir tanımlayalım bakalım.
using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Data.SqlClient;
namespace DAL
{
public class DataBase
{
#region Default Constructor
public DataBase()
{
}
#endregion
#region Properties
private string host;
public string Host
{
get { return host; }
set { host = value; }
}
private string dbName;
public string DbName
{
get { return dbName; }
set { dbName = value; }
}
private bool integratedSecurity;
public bool IntegratedSecurity
{
get { return integratedSecurity; }
set { integratedSecurity = value; }
}
private string userName;
public string UserName
{
get { return userName; }
set { userName = value; }
}
private string password;
public string Password
{
get { return password; }
set { password = value; }
}
private string others;
public string Others
{
get { return others; }
set { others = value; }
}
#endregion
#region Objects
private SqlConnection conn;
private SqlCommand cmd;
private SqlDataAdapter dap;
private DataTable dt;
#endregion
}
}
Bu property’lerden Others adında olan connection stringinize ek özellikler eklemek isterseniz bunu yapabilmeniz için eklenmiştir. Yani bağlantı cümlenizde yukarda tanımlı belli başlı özellikler dısında yeni bir şey eklemek isterseniz bunu others özelliğine string bir bilgi olarak ekleyebiliyorsunuz.
Property’lerimiz ve SqlConnection nesnemiz tanımlandıgına göre artık bunları kullanarak bir SqlConnection nesnesi yaratabiliriz. Bu amaçla aşagıdaki 3 methodu yazıyoruz. Bu methodlardan ilki overload edilmiş Default Constructor’ dır. String bir baglantı cümlesi alıyor ve buna göre hemen bir SqlConnection nesnesi oluşturuyor. Tabi biz sınıfımız oluştururken , SqlConnection nesnesi oluşsun istemiyorsak, nesneyi default constructor ile oluşturduktan sonra , gerekli olan server, database gibi bilgileri propertyler yardımı ile ekledikten sonra CreateDataBase methodu ile de aynı işlemi yapabiliyoruz. Bu methodu 2.overload versiyonu ise aynı constructordaki gibi tüm baglantı cümlesini string bir bilgi olarak alıyor ve buna göre bir SqlConnection oluşturuyor. Bu methodların 3 tane olması tamamen esneklik sağlamak amacıyla yapılmıştır. Yoksa mantıken yaptıkları işlem hepsinin aynı.
public DataBase(string con_str)
{
conn = new SqlConnection(con_str);
}
public bool CreateDatabase()
{
try
{
string conStr = "Server = " + host + "; DataBase = " + dbName + "; UID = + " + userName + "; Password = " + password + "; Integrated Security = " + integratedSecurity + "; " + others;
conn = new SqlConnection(conStr);
return true;
}
catch
{
return false;
}
}
public bool CreateDatabase(string con_str)
{
try
{
conn = new SqlConnection(con_str);
return true;
}
catch
{
return false;
}
}
Şimdi C# kodu içine gömülü SQL sorgularını çalıştıracak olan methodlarımızı yazabiliriz. Bu methodların 3 tane ve her birinin farklı şekilde çalışıtıgını yukarda söylemiştik.
public DataTable RunSqlTable(string sql)
{
if (cmd == null)
{
cmd = new SqlCommand();
}
cmd.Connection = conn;
cmd.CommandType = CommandType.Text;
cmd.CommandText = sql;
dap = new SqlDataAdapter(cmd);
dt = new DataTable();
dap.Fill(dt);
cmd.Dispose();
dap.Dispose();
return dt;
}
İlk olarak geriye sorgu sonucunu bir DataTable şeklinde döndüren methodumuza bir bakalım. Bu method bir string parametre alıyor. Aldıgı bu parametre sql kodunun ta kendisinidir. Methodumuz içerde eğer Command Nesnesi yaratılmadıysa yeni bir tane yaratıyor, ardından bağlantımıza bağlanıyor, ardından sql kodunu DataAdapter’ ın Fill Methodu ile bir DataTable’ a dolduruyor. Ve geriye bu DataTable’ ı döndürüyor. Bu işlemlerin yapılacagını ve temelde hep aynı olduklarını yukarda söylemiştim zaten. Gördüğünüz gibi ekstra olan veya farklı olan bir şey yok.
public void RunSqlQuery(string sql)
{
if (cmd == null)
{
cmd = new SqlCommand();
}
cmd.Connection = conn;
cmd.CommandType = CommandType.Text;
cmd.CommandText = sql;
conn.Open();
cmd.ExecuteNonQuery();
conn.Close();
cmd.Dispose();
}
RunSqlQuery ise yukardada dediğim gibi parametre olarak gelen sql sorgusunu çalıştırmakla yükümlüdür. Örnek vermek gerekirse bir DELETE veya UPDATE sorgusu. Bu sorgular gider veritabanı üzerinde işlem yapar ama geriye bir sonuc döndürmesi şart değildir.
public object RunSqlScalar(string sql)
{
if (cmd == null)
{
cmd = new SqlCommand();
}
cmd.Connection = conn;
cmd.CommandType = CommandType.Text;
cmd.CommandText = sql;
conn.Open();
object result = cmd.ExecuteScalar();
conn.Close();
cmd.Dispose();
return result;
}
RunSqlScalar methodu ise geriye tekil sonuç döndüren Sql sorgularının çalıştırılmasıyla yükümlüdür. Dikkat ederseniz command nesnemiz olan cmd ExecuteScalar() methodu ile sorguyu çalıştırmaktadır. ExecuteScalar() methodu geriye bir object döndürdüğü için bizim methodumuzun da return type’ ı object ‘tir. Bu degeri kullanacagımız zaman ilgili türe cast ederek kullancağız.
Bu 3 methodda kod içine gömülü sql kodlarını çalıştırıyordu. Yani bizim command nesemizin CommandType özelliği Text ‘ti dikkat ettiyseniz. Şimdi Stored Procedure’ler üzerinde işlem yapan 3 methodumuzu yazalım. Bu methodlar hakkında açıklama yazma gereği duymuyorum çünkü yukarda anlattıgım 3 method ile aynı işlevselliğe sahiptir, tek farklı bu sefer command nesnemizin CommandType özelliği Text değil, StoredProcedure ‘dür.
public DataTable RunSPTable(string sp_name)
{
if (cmd == null)
{
cmd = new SqlCommand();
}
cmd.Connection = conn;
cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandText = sp_name;
dap = new SqlDataAdapter(cmd);
dt = new DataTable();
dap.Fill(dt);
cmd.Dispose();
dap.Dispose();
return dt;
}
public void RunSPQuery(string sp_name)
{
if (cmd == null)
{
cmd = new SqlCommand();
}
cmd.Connection = conn;
cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandText = sp_name;
conn.Open();
cmd.ExecuteNonQuery();
conn.Close();
cmd.Dispose();
}
public object RunSPScalar(string sp_name)
{
if (cmd == null)
{
cmd = new SqlCommand();
}
cmd.Connection = conn;
cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandText = sp_name;
conn.Open();
object result = cmd.ExecuteScalar();
conn.Close();
cmd.Dispose();
return result;
}
İşte Stored Procedure’ler üzerinde işlem yapan methodlarımızda bunlar.Artık parametreler üzerinde işlem yapan methodlarımıza geçebiliriz. Bu arada belki dikkatinizi çekmiştir, neden her seferinde if(cmd == null) kontrolü yapıyoruz, bunun nedeni bu sınıfı kullanırken ilerde, parametre atama methodlarının, sql kodu calistirma methodlarından önce yazılıyor olmasıdır. Bu şu anlama gelir ben ozaman parametre atayacagım yerde bir command nesnesi yaratmalıyım. Ama ben sql kodu calistiran methodları çagırdıgım zaman null kontrolü yapmadan yeni bir command nesnesi oluşturursam önceden atadıgım parametreler yeni command nesnesinde olmayacaktır ve kodumuz calismayacaktır. Bu sebepten dolayı böyle bir önlem alarak bu sorunun önüne geciyoruz.
Şimdi parametre methodlarına bakabiliriz;
public void AddParameter(string name, object value)
{
if (cmd == null)
{
cmd = new SqlCommand();
}
cmd.Parameters.AddWithValue(name, value);
}
public void ClearParameters()
{
cmd.Parameters.Clear();
}
Aslında AddParameter yapı olarak command sınıfının AddWithValue methodundan pek farklı değildir. Zaten icerdede sizin methoda parametre olrak verdiğiniz parametre adını ve değerini AddWithValue methodu ile command nesnesinin parameters koleksiyonuna ekliyor.
ClearParameters methodu ise farklı bir sql sorgusunda kullanılmak üzere farklı parametreler tanımlanmadan önce var olan parametrelerin temizlenmesi amacıyla kullanılır.
Evet arkadaşlar bu işlemleri sırayla gerçekleştirdikten sonra artık projemizi derleyebiliriz. Yalnız projeyi derlerken Release modda derlemenizi tavsiye ederim. Bir proje sorunsuz bir şekilde tamanlandıktan sonra artık paketlenecek hale geldiyse bu modda derlenir.

Artık DAL.dll isimli dosyamız diğer projelerde kullanılmak üzere hazırdır J Test etmek amacıyla örnek bir windows application acıyoruz. Formumuzun üzerine;
1 tane DataGrid
4 tane TextBox
1 tane Button koyuyoruz.
Programımızın ilişkili çalışacagı veritabanının adı ise dbTest. İçinde tbl_Personel diye bir tablo ve, id, ad, soyad, maas adında 3 alan mevcuttur.
Şimdi DAL’ ımızı kullanarak bu veritabanına bağlanalım, tüm kayıtları cekelim, personel sayısını gösterelim ve yeni bir kayıt ekleyebilelim.
Öncelikle ilk yapmamız gereken DataAccessLayer projemizinin bulundugu dizine gidip, release klasorü icinden DAL.dll dosyamızı bulunması kolay bir yere kopyalayalım mesela masaüstü. Ardından window uygulamamıza geri dönüp, solution explorerda sağ tıklayıp add referance diyerek masaüstüne attıgımız dll dosyasını referanslarımıza ekleyelim.
Artık referansımız eklendiğine göre bu dll icindeki sınıfları yeni projemde rahatlıkla kullanabilirim. İlk olarak using deyimi ile referansım içindeki sınıflara direk erişebilmek için onu tanımlıyorum. Yoksa her sınıfa namespace adı ile erişmek zorunda kalacaktım.
Şimdi tüm kayıtları listelemek amacıyla form load ‘a şunları yazıyoruz.
private void Form1_Load(object sender, EventArgs e)
{
//DataBase db = new DataBase("Server=Localhost; DataBase = dbTest; Integrated Security = SSPI");
DataBase db = new DataBase();
db.Host = "Localhost";
db.DbName = "dbTest";
db.IntegratedSecurity = true;
db.CreateDatabase();
//db.CreateDatabase("Server=Localhost; DataBase = dbTest; Integrated Security = SSPI");
DataTable dt = db.RunSqlTable("SELECT * FROM tbl_Personel");
dataGridView1.DataSource = dt;
textBox4.Text = db.RunSqlScalar("SELECT COUNT(*) FROM tbl_Personel").ToString();
}
Gördüğünüz gibi veritabanına bağlanmanın 3 farklı yolu var. Öncelikle veritabanına bağlanıyoruz ve tüm kayıtları DataGridView ‘de gösteriyoruz. Bu işlem geriye bir DataTable döndürdüğü için ve C# kodu içine gömülü bir sql sorgusu oldugu için RunSqlTable methodunu kullandım.
Ardından tablodaki personel sayısını elde etmek için COUNT(*) ile geriye tekil bir sonuc döndüren RunSqlScalar() methodunu cagırdım ve geriye döndürdüğü personel sayısını TextBox4 e yazdırdım.
Son olarak button’ abasınca textboxlara girilen degerleri parametre olarak alıp bunları INSERT ile tabloya ekleyen bir Stored Procedure ‘ü kullanarak DAL’ ımızı test edelim. Bunun için Veritabanıma spPersonelEkle adından bir Stored Procedure yazıyorum.
CREATE PROCEDURE spPersonelEkle
@par_ad nvarchar(50), @par_soyad nvarchar(50), @par_maas int
AS
BEGIN
INSERT INTO tbl_Personel (ad, soyad, maas) VALUES (@par_ad, @par_soyad, @par_maas)
END
GO
Ardından buton ‘un click olayına aşagıdaki kodları yazıyorum.
private void button1_Click(object sender, EventArgs e)
{
DataBase db = new DataBase("Server=Localhost; DataBase = dbTest; Integrated Security = SSPI");
db.AddParameter("@par_ad", textBox1.Text);
db.AddParameter("@par_soyad", textBox2.Text);
db.AddParameter("@par_maas", Convert.ToInt32(textBox3.Text));
db.RunSPQuery("spPersonelEkle");
DataTable dt = db.RunSqlTable("SELECT * FROM tbl_Personel");
dataGridView1.DataSource = dt;
db.ClearParameters();
}
Gördüğünüz gibi normalde her seferinde SqlCommand veya DataAdapter’ lar la program yazmak yerine bunları 1 kereliğine bir DAL olarak yazıyoruz ve ardından her projede kullanabiliyoruz. Bu işte bize büyük bir esneklik ve kolaylık sağlamaktadır gerçekten.
Bir makalemizin daha sonuna geldik arkadaşlar. Bu yapıyı geliştirmek kendinize göre uyarlamak tamamen size bağlı. Amacım bu işin mantıgını anlatabilmekti. Umarım sizler için faydalı bir yazı olmuştur.
Projenin kaynak dökümanlarını buradan download edebilirsiniz.
http://rapidshare.de/files/39251710/dal.rar.html
Mehmet Aydın Ünlü
aydinunlu85@gmail.com
http://www.aydinunlu.blogspot.com
3 yorum:
teşekkürler, çok faydalı bir yazı :)
Teşekkürler
Teşekürler Mehmet bey, çok açık ve net bir yazı ancak source linki ölü yenilerseniz memnun oluruz.
Yorum Gönder