Derinlemesine yazılım eğitimleri için kanalımı takip edebilirsiniz...

C# Unit of Work Design Pattern(Unit of Work Tasarım Deseni)

Merhaba,

Bu içeriğimizde veritabanı işlemlerini toplu halde gerçekleştirmemizi sağlayan ve olası bir hata durumunda topyekün geri alınabilmesine olanak tanıyan bir kurumsal tasarım kalıbı olan Unit Of Work Design Pattern’ı inceliyor olacağız.

Unit Of Work Design Pattern Nedir?

Unit Of Work tasarım deseni, yazılım uygulamamızda veritabanıyla ilgili her bir aksiyonun anlık olarak veritabanına yansıtılmasını engelleyen ve buna nazaran tüm aksiyonları biriktirip bir bütün olarak bir defada tek bir connection üzerinden gerçekleştirilmesini sağlayan ve böylece veritabanı maliyetlerini oldukça minimize eden bir tasarım desenidir.

Yukarıda veritabanı maliyetlerinin minimize edilmesinden bahsettik. Bu ifadeyi konumuzun esintisi açısından biraz daha açarsak eğer şöyle izahatte bulunabiliriz; .NET çatısı altında kullanılan veritabanı mimarisi Ado.NET yahut herhangi bir ORM(örn; Entity Framework) aracı ile veritabanına yapılan herhangi bir insert, update yahut delete isteği varsayılan olarak bir transaction içerisinde execute edilmektedir. Hal böyleyken her bir istek oldukça maliyetli bir şekilde işlenecektir. Neden mi? Çünkü, transaction mekanizması veritabanında oluşan tüm aktiviteleri takip edebilmekte ve böylece olası bir aksaklık, hata ya da süreçte tarafımızca belirlenmiş şartlar doğrultusunda yapılan tüm işlemleri geri alabilmektedir. Dolayısıyla transaction bu sorumluluğu karşılıksız yapmayacağından dolayı faturayı yüksek maliyet olarak kesmektedir. Bunun yanında her bir sorguya özel bir transaction devreye girdiğini düşünürsek maliyetimiz kat be kat artmakta ve veritabanını inanılmaz ölçüde yormaktadır. İşte nedeni budur!

Peki bu maliyeti minimize edecek çözüm nedir?
Bu sorunun cevabını verebilmek için transaction yapılanmasının genellikle bilinen bir özelliğine tekrar değinmek gerekmektedir. Transaction, içerisinde bir veya birden fazla sorguyu kontrol edebilme kapasitesine sahip mekanizmadır. Dolayısıyla her bir operatif sorgu için transaction devreye sokmaktansa tüm sorgularımızı kapsayacak bir adet transactionın devreye girmesi maliyeti oldukça minimize edecektir. İşte bu çözümün teknik adı Unit Of Work’tür.

Unit Of Work tasarımına tam teferruatlı giriş yapmadan önce ilk olarak ORM araçlarından Entity Framework Core kullanarak gönderdiğimiz sorguları SQL Server Profiler uygulamasından takip edelim ve her istekte oluşturulan ve bize yüksek maliyete sebep olan transactionları Unit Of Work tasarımının kıymetini anlayabilmek için gözlerimizle görelim 🙂

    [ApiController]
    [Route("api/[controller]")]
    public class CompanyController : ControllerBase
    {
        CompanyContext _companyContext;
        public CompanyController(CompanyContext companyContext)
        {
            _companyContext = companyContext;
        }
        public bool DatabaseBusiness()
        {
            _companyContext.Add(new Company
            {
                Name = "GençAy A.Ş"
            });
            _companyContext.SaveChanges();

            _companyContext.Add(new Company
            {
                Name = "NG A.Ş"
            });
            _companyContext.SaveChanges();

            _companyContext.Products.AddRange(new List<Product> {
                new Product{  CompanyId = 1, Name = "X", Quantity = 10 },
                new Product{  CompanyId = 1, Name = "Y", Quantity = 11 },
                new Product{  CompanyId = 1, Name = "Z", Quantity = 12 },
                new Product{  CompanyId = 2, Name = "A", Quantity = 13 },
                new Product{  CompanyId = 2, Name = "B", Quantity = 14 },
            }.ToArray());
            _companyContext.SaveChanges();

            return true;
        }
    }

Yukaridaki örnek “Company(Controller)” sınıfına göz atarsanız eğer “DatabaseBusiness” metodu içerisinde birden fazla veritabanı işlemi gerçekleştirilmekte ve süreçte her bir işlem “SaveChanges” metodu ile execute edilmektedir. İlgili endpointe yapılan request neticesinde veritabanı sunucusunda generate edilen SQL kodlarını izleyebilmek için Microsoft SQL Server Management Studio editörüyle beraber yüklenen SQL Server Profiler uygulamasını açınız ve ardından aşağıdaki görselde belirtilen talimatları uygulayınız.

C# Unit of Work Design Pattern(Unit of Work Tasarım Deseni) Hangi sunucuda çalışıyorsanız ona bağlanınız.
İlgili sunucuda execute edilen sorguların çalıştırıldığı transactionları görebilmek için Profiler uygulamasında transactionlara özel ayarlama yapmamız gerekmektedir. Bunun için karşımıza gelen Trace Properties penceresinde Events Selection sekmesine geçerek sağ altta bulunan Show all events kutucuğunu işaretleyerek tüm özellikleri listeleyiniz ve oradan Transactions kategorisi altındaki uygun alanları seçerek görülebilir hale getiriniz. C# Unit of Work Design Pattern(Unit of Work Tasarım Deseni)
Ve tüm bu işlemler neticesinde aşağıdaki ekran görüntüsünde olduğu gibi sunucuya gelen her bir istek neticesinde transaction oluşturulduğunu gözlemleyiniz.
C# Unit of Work Design Pattern(Unit of Work Tasarım Deseni)

Evet… Yukarıda görüldüğü üzere her bir SaveChanges SQL sunucusunda bir transaction oluşturmakta ve içerisinde işlem yapmaktadır. Nihayetinde bizim amacımız burada şahit olduğumuz maliyeti minimize etmektir. “La hoca yeter artık gir şu Unit Of Work denen zımbırtıya” dediğinizi duyar gibiyim 🙂 Tamam. Malum konuya gireceğiz merak etmeyin 🙂 Biraz daha sabretmenizi rica ediyorum 😉

Yukarıdaki örneğimize geri dönecek olursak, her bir SaveChanges metodunda bir transaction oluşturuyorsak eğer çözüm olarak her bir işlem neticesinde değil tüm işlemler neticesinde tek bir SaveChanges çağırmayı tercih edebiliriz. Bunun için metot içeriğini aşağıdaki gibi düzenleyelim ve ardından test edelim;

    [ApiController]
    [Route("api/[controller]")]
    public class CompanyController : ControllerBase
    {
        CompanyContext _companyContext;
        public CompanyController(CompanyContext companyContext)
        {
            _companyContext = companyContext;
        }
        public bool DatabaseBusiness()
        {
            _companyContext.Add(new Company
            {
                Name = "GençAy A.Ş"
            });

            _companyContext.Add(new Company
            {
                Name = "NG A.Ş"
            });

            _companyContext.Products.AddRange(new List<Product> {
                new Product{  CompanyId = 1, Name = "X", Quantity = 10 },
                new Product{  CompanyId = 1, Name = "Y", Quantity = 11 },
                new Product{  CompanyId = 1, Name = "Z", Quantity = 12 },
                new Product{  CompanyId = 2, Name = "A", Quantity = 13 },
                new Product{  CompanyId = 2, Name = "B", Quantity = 14 },
            }.ToArray());
            _companyContext.SaveChanges();

            return true;
        }
    }

C# Unit of Work Design Pattern(Unit of Work Tasarım Deseni)

Görüldüğü üzere tüm operasyonları tek bir SaveChanges ile execute ettiğimizde tek bir transaction ile kontrol edilmekte ve süreç daha az maliyetli ve sağlıklı bir hale getirilmiş bulunmaktadır. İşte, Unit Of Work desenide aslında bu mantığın ta kendisini benimsemekte ve tüm işlemleri tek seferde tek transaction içerisinde execute etmeye odaklanmaktadır.

Tabi ki de Unit Of Work tasarım deseni kullanılırken bu şekilde bir kullanımdan ziyade daha kompleks bir tasarım içerisine yerleştirilmektedir. O halde şimdi gelin Unit Of Work’ün hangi tasarımlarla nasıl çalıştığını ele alalım…

Unit Of Work Nasıl Çalışır?

C# Unit of Work Design Pattern(Unit of Work Tasarım Deseni)
Unit Of Work, toplu veritabanı işlemlerini tek seferde bir kereye mahsus execute eden ve böylece bu toplu işlem neticesinde kaç kayıtın etkilendiğini rapor olarak sunabilen bir tasarım desenidir. Genellikle Repository Design Pattern ile birlikte kullanılması tercih edilen Unit Of Work, ayrıca (genellikle) transaction kontrolüyle beraber kullanılarak tek bir merkezden tüm sorgu süreçlerini kontrol edebilmektedir. Tüm bunların yanında, kullanıcı tarafından adım adım(zincirleme) yapılan operasyonlarda süreç tam teferruatlı sonlandırılmadan vazgeçildiği taktirde o noktaya kadar yapılan tüm değişikliklerin geriye alınması gerekmektedir. İşte buradaki iş maliyetini Unit Of Work ortadan kaldırmakta ve zincir sonlanmaksızın hiçbir entitynin değerini veritabanında fiziksel olarak değiştirtmemektedir.

Repository Design Pattern İle Unit Of Work Kullanımı?

Biz bu makalemizde Unit Of Work tasarım desenini transaction kontrolüyle birlikte Repository deseniyle bütünleşik olarak ele alacağız.
İlk olarak Repository imzamızı oluşturalım;
Dikkat! Kodu şişirmemek adına basit işlemleri yapan bir Repository tasarladım

    public interface IRepository<T> where T : class
    {
        List<T> Get();
        bool Add(T model);
        bool Add<A>(A model) where A : class;
        bool Remove(T model);
        int Save();
    }

Ardından Unit Of Work imzamızı oluşturalım;

    public interface IUnitOfWork : IDisposable
    {
        bool Commit(bool state = true);
    }

Şimdi Repository sınıfımızı oluşturalım ve gerekli yapılanmanın ardından Unit Of Work stratejisini uygulayalım.

    public class Repository<T> : ControllerBase, IRepository<T>, IUnitOfWork where T : class
    {
        protected CompanyContext _companyContext;
        IDbContextTransaction transaction = null;
        public Repository(CompanyContext companyContext)
        {
            _companyContext = companyContext;
            transaction = _companyContext.Database.BeginTransaction();
        }
        [NonAction]
        public List<T> Get() => _companyContext.Set<T>().ToList();
        [NonAction]
        public bool Add(T model)
        {
            _companyContext.Set<T>().Add(model);
            return true;
        }
        [NonAction]
        public bool Add<A>(A model) where A : class
        {
            _companyContext.Set<A>().Add(model);
            return true;
        }
        [NonAction]
        public bool Remove(T model)
        {
            _companyContext.Set<T>().Remove(model);
            return true;
        }
        [NonAction]
        public int Save() => _companyContext.SaveChanges();
        [NonAction]
        public bool Commit(bool state = true)
        {
            Save();
            if (state)
                transaction.Commit();
            else
                transaction.Rollback();

            Dispose();
            return true;
        }
        public void Dispose()
        {
            _companyContext.Dispose();
        }
    }

Yukarıdaki kod bloğunda 4. satıra odaklanırsanız eğer 8. satırda context üzerinden başlatılarak elde edilen transaction “IDbContextTransaction” tipinden referans tarafından işaretlenmektedir. Entity Framework yapılanmasında context üzerinden ne zaman BeginTransaction() metodu ile bir transaction başlatılsın, bu başlatılan transaction devrede olacağından dolayı sorgu isteklerinde otomatik oluşturulan transactionlar ezilmiş olacaktır. Dolayısıyla süreçte bize eşlik edecek transaction elimizde bulunmaktadır ve tarafımızca kontrol edilebilir bir özelliktedir. 33. satırda ise Commit metodu aracılığıyla hem Save fonksiyonu çalıştırılmakta hemde mevcut transaction optional parameter değerine göre ya Commit edilmekte ya da Rollback ile tüm işlemler geri alınmaktadır.

Bu çalışma neticesinde mevcut Controller sınıflarımızı ilgili Repository sınıfından türetelim ve aşağıdaki çalışmaları gerçekleştirelim.

    [ApiController]
    [Route("api/[controller]")]
    public class CompanyController : Repository<Company>
    {
        public CompanyController(CompanyContext companyContext) : base(companyContext)
        { }
        public bool DatabaseBusiness()
        {
            Add(new Company
            {
                Name = "GençAy A.Ş"
            });
            Add(new Company
            {
                Name = "NG A.Ş"
            });

            Add(new Product { CompanyId = 1, Name = "X", Quantity = 10 });
            Add(new Product { CompanyId = 1, Name = "Y", Quantity = 11 });
            Add(new Product { CompanyId = 1, Name = "Z", Quantity = 12 });

            Add(new Product { CompanyId = 2, Name = "A", Quantity = 13 });
            Add(new Product { CompanyId = 2, Name = "B", Quantity = 14 });

            Commit();

            return true;
        }
    }

C# Unit of Work Design Pattern(Unit of Work Tasarım Deseni)

Görüldüğü üzere Unit Of Work tasarım deseni sayesinde tüm isteklerimiz tek bir seferde veritabanına gönderilmekte ve hepsi manuel bir şekilde başlattığımız transaction ile yönetilmektedir. Böylece maliyet oldukça düşmekte ve dolaylı yoldan gözle görülür bir performans artışı söz konusu olmaktadır. Ayriyetten burada endpointlerin Commit fonksiyonunu çağırma sorumluluğunu üstlenmesi dikkat edilmesi gereken bir husustur. Nihayetinde kaz gelecek yerden tavuk esirgenmez misali, onca performans getirisine o kadarcık zahmet göz ardı edilebilir olsa gerektir 🙂

Son Dedikodular

Unit Of Work tasarımına istinaden internette yapılan araştırmalarda karşınıza özellikle mühimlik teşkil eden iki hususta yorumlar gelebilir. Bu yorumlardan ilki Entity Framework ile Unit Of Work tasarımının kullanılmasının pekte tavsiye edilmemesi üzerinedir. Bunun nedeni, Entity Framework’ün kendi bünyesinde Unit Of Work barındırmasından dolayıdır. Hatta bu konuda ciddi ciddi içerikler oluşturup muhalif olanlar bile vardır. Bu konu üzerine benim nacizane tavsiyem Unit Of Work kullanım olarak Entity Framework’ün bünyesine çok fazla temas etmediği taktirde gönül rahatlığıyla kullanılabilir. Bir diğer husus ise internetteki birçok kaynakta context nesnesinin Dispose edilmesi üzerinedir. Evet, bizde bu içeriğimizde Dispose etmiş bulunmaktayız. Lakin stackoverflow’da ki şu arkadaşında söylediği gibi Entity Framework Core yapılanması Dependency Injection ile elde edilen context nesnesini otomatik dispose etmekte ve bu işlemi bizim yapmamıza gerek kalmamaktadır. Ve bu mantığa istinaden bizler context nesnesini yine dispose etmeyi lakin bu işlemi gerçekleştirirken pür dikkat olmayı unutmamalıyız.

İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İyi çalışmalar…

Bunlar da hoşunuza gidebilir...

14 Cevaplar

  1. Enes Taha SELEK dedi ki:

    Sadece ve akıcı anlatım için teşekkürler…

  2. Murat dedi ki:

    Hocam merhaba, öncelikle açıklayıcı anlatımınız için teşekkürler. Benim bir sorum var. Yazının başlarında da değindiğiniz gibi bu vertabanı işlemlerini sıralayıp en sonda SaveChanges metodunu çağırdığımızda da aynı durumu elde etmiş olmuyor muyuz? Aynı işi yapıyorlarsa bu kadar kod yazmamızın bir getirisi olmalı mutlaka yoksa fazladan kod maliyeti oluşturur. Bu konu hakkındaki fikirlerinizi merak ediyorum. Teşekkürler tekrardan.

    • Gençay dedi ki:

      Merhaba,

      Esasında Unit of Work tasarım deseni SaveChanges sürecindeki transaction kontrolünü Repository tasarım deseniyle daha da efektif hale getirerek tam olarak ele almamızı sağlayan bir stratejidir. Yani burada anahtar kelime olarak Repository ile transaction terimlerini seçersek bu ikisinin bileşimidir diyebiliriz.

      Sevgiler.

  3. Tahsin dedi ki:

    Hocam merhaba, Benim anlayamadığım bir nokta var. Siz 2 tane Company eklediniz ve Productları eklerken companyId leri manuel verdiniz. Biz CompanyId leri dinamik olarak vermek istesek nasıl bir yol izlememiz gerekir. Burası UnitOfWork Desing patternin’da içinden çıkamadığım bir nokta yardıcı olursanız çok sevinirim. (Sonuçta yeni oluşmuş Company’nin Id sini bilmiyor olacağız veya CompanyId’yi İnt değil de guid olarak tutuyoruz.)
    Saygılarımla.

  4. Tahsin dedi ki:

    Hocam merhaba;
    Benim kafama takılan bir nokta var. Siz CompanyId leri manuel olarak vermişsiniz. Ben CompanyId’leri dinamik olarak vermek istersem nasıl bir yol izlemem gerekir.
    Teşekkürker.

    • Gençay dedi ki:

      Merhaba,

      Bu konuda Entity Framework’ün nimetlerinden faydalanabilirsiniz.

      Şöyle ki;
      Bir şekilde company’i context’ten sorgulayarak elde edin.

      Company company = _companyContext.Companies.FirstOrDefault(c => c.Id == 3);
      

      Ardından ilgili company nesnesine aşağıdaki işlemi uygulayın.

      company.Products.AddRange(new List<Product> {
                  new Product{ Name = "X", Quantity = 10 },
                  new Product{ Name = "Y", Quantity = 11 }
              }.ToArray());
      

      Böylece X ve Y isimli product’lar ilgili company ile ilişkisel bir şekilde veritabanına kaydedilecektirler.

      Kolay gelsin.

  5. Gençay dedi ki:

    Okuyucularımın dikkatine;

    UnitOfWork tasarımının ‘SaveChanges’ metodunun işlevselliğiyle olan benzerliğine dair gelen yoğun sorulardan dolayı olayı daha net yorumlayabilmenizi ve bütünsel bir şekilde görebilmenizi sağlayacak olan farklı bir tasarım sunmak istiyorum.

    Bu tasarımda IUnitOfWork interface’i uygulamada kullanılacak olan tüm repository sınıfları için bir arayüz görevi görmekte ve transaction kontrolünü direkt olarak kendi üzerinden sağlamaktadır.

    Şöyle ki;

    IUnitOfWork;

        public interface IUnitOfWork : IAsyncDisposable
        {
            Task<IDbContextTransaction> BeginTransactionAsync();
            public ICategoryRepository Categories { get; }
        }
    

    UnitOfWork;

        public class UnitOfWork : IUnitOfWork
        {
            private readonly YazilimEgitimiContext _context;
            public UnitOfWork(YazilimEgitimiContext context, ICategoryRepository categoryRepository)
            {
                _context = context;
                Categories = categoryRepository;
            }
            public ICategoryRepository Categories { get; }
            public async Task<IDbContextTransaction> BeginTransactionAsync() => await _context.Database.BeginTransactionAsync();
            public async ValueTask DisposeAsync() => await _context.DisposeAsync();
        }
    

    Örnek bir servis;

        public class CategoryService : ICategoryService
        {
            private readonly IUnitOfWork _unitOfWork;
            public CategoryService()
            {
                _unitOfWork = ProviderService.GetUnitOfWork();
            }
            public async Task<IEnumerable<Category>> GetAllCategoriesAsync() => await _unitOfWork.Categories.GetAllAsync();
            public async Task<Category> GetByIdCategoryAsync(string id) => await _unitOfWork.Categories.GetSingleAsync(c => c.Id == id);
            public async Task<(bool, Category)> CreateAsync(Category_Create_VM model)
            {
                using IDbContextTransaction transaction = await _unitOfWork.BeginTransactionAsync();
                var resultData = await _unitOfWork.Categories.AddAsync(model);
                await transaction.CommitAsync();
                return resultData;
            }
            public async Task<bool> DeleteAsync(string id)
            {
                using IDbContextTransaction transaction = await _unitOfWork.BeginTransactionAsync();
                Category deleteCategory = await _unitOfWork.Categories.GetSingleAsync(c => c.Id == id);
                bool result = await _unitOfWork.Categories.DeleteAsync(deleteCategory); ;
                await transaction.CommitAsync();
                return result;
            }
            public async Task<bool> UpdateAsync(Category model)
            {
                using IDbContextTransaction transaction = await _unitOfWork.BeginTransactionAsync();
                bool result = await _unitOfWork.Categories.UpdateAsync(model);
                await transaction.CommitAsync();
                return result;
            }
        }
    

    Böylece UnitOfWork transaction kontrolünü daha sağlıklı bir şekilde gerçekleştirmekte ve başlatılmış yahut sonlandırılmış transaction’ların hatalarından bizleri arındırmaktadır.

    Nihai sonuç olarak UnitOfWork tasarımında bu stratejiyi uygulamanızı tavsiye ediyorum.

    İyi çalışmalar dilerim…

  6. baran dedi ki:

    Merhaba,uzun süredir repository pattern ve uow ile ilgili aklıma takılan şu soru var; projemde çalışırken bir tablodan veri çekerken skip-take işlemi yapmam gerekiyor, bunu uow ile yapmak istediğim zaman repositorylerime ulaşıp “GetAll” metodundan tüm verileri çekip daha sonra skip-take işlemi yapıyorum mecburen ve bu performans kaybı yaşatıyor. direkt Sql’e istediğim query’i göndermem lazım aslında ama ben tüm veriyi çekip içinde dolaşıyorum. bu patternlar ile benim istediğimi yapmak mümkün değil gibi duruyor. alternatif olarak nelere göz atabilirim_?

    • Gençay dedi ki:

      Merhaba,

      IQueryable ve IEnumerable konularını incelemeni ve bu pattern’ı kullanırken EF’nin IQueryable parametrelerinde çalışmana dikkat etmeni öneririm.

      Kolay gelsin.

      • baran dedi ki:

        IQueryable srogular atmam lazım benim zaten ama demek istediğim bu patternlarda “IQueryable sorgular kullanmak doğru değil yoksa amacının dışına çıkarsınız vs.” gibi yorumlar okudum birkaç yerden. Yeni bir projede artık bu IEnumerable yapısından kurtulmak istiyorum yeri geldiğinde. Aklımda singleton patternini kullanarak bir db nesnesi üretip onu kullanmak bu sayede en azından servis katmanımda queryable çalışabilirim ama db yi businessa katmak ne kadar doğru olur bunu bilemedim.kafam karışık..

        • Gençay dedi ki:

          Hayır, okuduğunuza katılmıyorum. Nihayetinde ihtiyaca binaen IQueryable ile çalışacaksanız bunu benimsediğiniz tasarım desenine güzelce giydirme ihtimaliniz varken, sırf birileri önermedi diye farklı bir şekilde çözüm getirmeye çalışmak pek mantıklı gelmedi. Ayriyetten piyasadaki kurumsal birçok örnekte Repository’nin IQueryable temellerinde inşa edildiğini gözlemleyebilirsiniz.

          IQueryable’da çalışmak için pek fazla işlem yapmanıza gerek yok. Repository arayüzünde Expression türünü aşağıdaki gibi kullanmanız yeterli olacaktır :

          IEnumerable;

          public interface IRepository<T> where T : class
          {
              List<T> GetWhere(Func<T, bool> metot);
              T GetSingle(Func<T, bool> metot);
          }
          

          IQueryable;

          
          public interface IRepository<T> where T : class
          {
              List<T> GetWhere(Expression<Func<T, bool>> metot);
              T GetSingle(Expression<Func<T, bool>> metot);
          }
          

          Gerisini Entity Framework Core halledecektir.

          Kolay gelsin.

  7. Çağatay dedi ki:

    Selam,
    Şu videolu resimli gösterimi hangi programda yapıyorsunuz?

    Teşekkürler

    • Gençay dedi ki:

      Merhaba,

      Sanırım .gif uzantılı dosyaları kastediyorsunuz. Herhangi bir video kayıt uygulamasıyla ekran görüntüsünü .mp4 vs. gibi bir uzantıyla alıp, ardından .gif formatına convert ediyorum 🙂 Gif’e dönüşüm için şu adresteki uygulamayı kullanıyorum. Ve eminim ki daha pratik bir yolu vardır ama bizimkisi alışkanlık işte 🙂

      Kolay gelsin…

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

*