30 Ocak 2011 Pazar

Entity Framework Code First

EF 4 ile birlikte birçok yeni özellik gelmiştir.

  • POCO: Entity sınıflarının bir üst sınıftan türetilmek zorunda değiller
  • Lazy Load: Entity örneği alt Entity özelliklerini ilk erişim anında yükleyebilirler
  • Self-Tracking: Entity örnekleri değişim durumlarını kendileri takip ediyorlar. Böylece durum bağımsız (stateless) web çağrılarında kullanılabilirler.
  • Test: EF 4 nesneleri interface’ler aracılığı ile daha kolay test edilebilir durumdalar
  • Linq: Artık EF 4 tüm linq metotlarını destekliyor

Visual Studio içinde EF 4 için birçok model tasarımcısı ve araç eklenmiştir. EF tasarımcıları “Model First” olarak adlandırılan yöntem için kullanılırlar. Model First önce veri tabanında veya EF 4 model tasarımcısında veri modelin çıkartılması daha sonra çıkartılan modele ait kodların kod şablonları (T4) ile oluşturulması yöntemidir.

Code First

Entity Framework 4 ile tasarımcı temelli Model First veya Database First yaklaşımına ek olarak “Code First” olarak adlandırılan kod merkezli yeni bir yaklaşım ortaya konmuştur. Code First ile her hangi bir tasarımcıya, xml eşleştirme dosyasına veya bir Entity Base Class’a ihtiyacınız yoktur. Model ile Entity sınıflarınız arasında ki eşleştirmeyi isimlendirme kuralları ile yapabilirsiniz ve bu eşleştirmeyi tamamen kodlama ile özelleştirebilirsiniz.

Uygulama Geliştirme

Öncelikle bir uygulama oluşturalım. Daha sonra Nuget ile projemize EFCodeFirst ekleyelim.

Bir veri tabanı ile başlamamıza gerek yok. Bunun yerine veri yapımızı (Domain Model ) taşıyan standart NET sınıfları yazarak işe başlayacağız.

Model Sınıflarını Oluşturma

Küçük bir adres defteri uygulaması yazıyoruz ve bizim veri yapımız gerçekten küçük bir yapıdır. Veri modelimizde sadece Kişiler ve Kişi Adresleri var. Bu basit iki sınıfı oluşturalım.

[cs]

public  class  Person 
 {
     public  int  PersonId  { get ; set ; }
     public  string  FullName  { get ; set ; }
     public  string  Company  { get ; set ; }
     public  DateTime  BirthDay  { get ; set ; }
     public  virtual  List  Adresses  { get ; set ; }
 }
 public  class  Adress 
 {
     public  int  AdressId  { get ; set ; }
     public  string  AdressType  { get ; set ; }
     public  string  FullAdress  { get ; set ; }
     public  int  PersonId  { get ; set ; }
     public  virtual  Person  Person  { get ; set ; }
 }

Yukarıda ki Person ve Adress model sınıfları POCO sınıflardır. Her hangi bir üst sınıftan veya interface’ den türetmek zorunda değiliz. Her hangi bir veri tabanı bağlantısı da belirtmek zorunda değiliz. Veri modeline uyan herhangi bir veri tabanına bağlanabilirler.

Context Sınıfını Oluşturma

POCO sınıflarımızı oluşturduk. Veri tabanımız ile POCO sınıflarımızı eşleştirme işlemini yapacak Context sınıfımızı yazabiliriz.

[cs]

public  class  AdressBook  : DbContext 
 {
     public  DbSet  Persons  { get ; set ; }
     public  DbSet  Adresses  { get ; set ; }
 }

AdressBook eşleştirme sınıfının DbContext sınıfından türetilmelidir. AddressBook sınıfı iki public özelliği Persons ve Adresses özellikleride DbSet sınıfının uygulamalarıdır. DbContext ve DbSet sınıfları EF Code First kütüphanesinde yer almaktadır.

İhtiyacımız olan tüm kodlamayı yaptık. Veri tabanı ile eşleştirme için başka bir işleme ihtiyacımız yok. Her hangi bir veri tabanı şema eşleştirme dosyası veya eşleştirme aracı veya her hangi bir tasarımcı kullanmayacağız. Veri tabanı üzerinde gerekli tüm işlemleri bu üç sınıf yapacak.

Kural Tabanlı Eşleştirme

Her hangi bir eşleştirme verisi olmadan bu üç sınıf nasıl veri tabanında ki nesneler ile eşleşecekler?

EF Code First “yapılandırma yerine kurallar” yaklaşımı ile eşleştirme yapmaktadır. Tabi ki bu varsayılan kuralları değiştirebilirsiniz.

Nedir EF Code First kuralları?

EF Code First “sadece çalış” mantığı ile mümkün olduğu adar az veri ile çalışabilecek şekilde tasarlanmıştır. Mesela Adres defteri örneğimizde Persons ve Adress olarak iki tablo oluşturacaktır. Bu tabloların kolonları da sınıfların property’lerinden oluşacaktır. Primary key olarak da tablo adı ile aynı sonu Id ile biten property kullanılacaktır. Foreing Key olarakda sınıf içinde ki diğer nesne referansları kullanılacaktır.

Veri Tabanı

Kodlarımız yazdık. Peki ya veri tabanı? Kodları yazarken hiçbir veri tabanına ihtiyacımız yok. Fakat çalışma anında veri tabanı gerekmektedir. İki şekilde veri tabanı oluşturabiliriz. İlki kendimiz gider uygun veri tabanını oluştururuz. İkincisi EF Code First bizim yerimize otomatik olarak tüm veri tabanını oluşturur. Adres defteri uygulamamız için ikinci yolu tercih ediyorum. EF Code First varsayılan olarak DbContext sınıfı ile aynı isimde bir bağlantı adı arayacaktır. Bizim DbConext sınıfımızın adı AdressBook olduğu için aynı isimde bir bağlantı adı kurmalıyız.

   <connectionStrings>
     <add name= "AdressBook "
          connectionString= "Data Source=|DataDirectory|AdressBookDb.sdf "
          providerName= "System.Data.SqlServerCe.4.0 " />
   </connectionStrings>

Bu örnekte ben veri tabanı olarak SQL CE veri tabanında faydalandım.

Model Nasıl Kullanılır?

Tüm ihtiyacımızı karşılayan üç sınıfı yazdık. Linq ile istediğimiz tüm sorguları çalıştırabiliriz.

Tüm kişi listesini getiren bir linq expression yazalım:

[cs]

var  adressBook = new  AdressBook ();
 var  list = from  p in  adressBook.Persons 
                 orderby  p.FullName 
                 select  p;

İki tablo arasında ki ilişkiden de faydalanabiliriz:

[cs]

var  adressBook = new  AdressBook ();
 var  list = from  p in  adressBook.Persons 
             where  p.Adresses .Count  > 0
             orderby  p.FullName 
             select  p;

Yukarıda ki kodda sadece adres verisi olan kişiler listelenmiştir. Primary Key özelliğinden de faydalanabiliriz:

[cs]

var  adressBook = new  AdressBook ();
 var  person = adressBook.Persons .Single (c => c.PersonId  == id);

CRUD işlemleri olmadan olmaz. Yeni bir kişi eklemek için yeni bir Person nesnesi oluşturup gerekli property’leri doldurmalıyız. Daha sonra DbContext üzerinde ki ilgili DbSet’e yeni kişi nesnemizi eklemeliyiz. Son olarak da yaptığımız değişikliği veri tabanına yansıtmamız gerekmektedir.

[cs]

var  person = new  Person 
 {
     FullName  = "Ali Veli" , 
     Company  = "ana okulu" , 
     BirthDay  = DateTime .Today 
 };
 var  adressBook = new  AdressBook ();
 adressBook.Persons .Add (person);
 adressBook.SaveChanges ();

Kayıt güncellemek için önce güncelleyeceğimiz kaydı buluyoruz. Sonra gerekli değişikliği yapıyoruz. En son yapılan değişikliği veri tabanına yansıtıyoruz.

[cs]

var  adressBook = new  AdressBook ();
 var  person = adressBook.Persons .Find (id);
 person.Company  = "FOG" ;
 person.BirthDay  = DateTime .Today .AddYears (20);
 person.FullName  = "Deli Veli" ;
 adressBook.SaveChanges ();

Sonuç

ORM araçları iş uygulaması geliştirmenin en önemli parçasıdır. EF 4 Code First veri ile kod temelli çalışılabilecek bir yol sunmaktadır. EF Code First çok güçlü bir veri işleme yöntemidir. Bu yazıda EF Code First varsayılan kuralları ile ele alınmıştır. Burada değinilmemiş birçok özellik var. System.ComponentModel.DataAnnotations isim uzayında var olan nitelikler ile entity nesnelerinize doğrulama özellikleri ekleyebiliriz. IValidatableObject arayüzü ile sınıf seviyesinde doğrulama metotları ekleyebiliriz. DbContext sınıfının OnModelCreating metodu ile entity ve veri nesnelerini eşleştirmelerini özelleştirebilirsiniz. (Böylece mevcut veri tabanları ile EF Code First yaklaşımını kullanabilirsiniz. ) DatabaseIntialize sınıfları ile veri tabanı oluşunca eklenecek varsayılan kayıtları girebilirsiniz vs.

EF Code First tümleşik bir veri işleme yöntemi sunmaktadır. EF Code First ile kod geliştiriciler daha aktif kullanılabilen bir veri işleme ortamına sahip oldular.


28 Ocak 2011 Cuma

SQL CE

SQL CE kolay veritabanı depolaması sağlayan ücretsiz, gömülü, veritabanı motorudur. SQL CE kullanmak için her hangi bir veri tabanı kurulumu gerektirmez. Sadece gerekli dll’ler bin dizinine kopyalanır ve uygulamanız veri tabanını motorunu kullanır. SQL CE çalışma döngüsü uygulamanın veri tabanına ilk erişim ile başlar ve uygulama kapatılınca uygulama ile birlikte kapatılır. Uygulama ile birlikte kapanan SQL CE veri tabanı otomatik olarak bellekten temizlenir.

SQL CE veri tabanı verileri dosya içinde saklamaktadır. SQL CE veri tabanını sadece dosyayı kopyala-yapıştır yaparak taşıyabilirsiniz. Dosyada verileri saklaması rağmen ASP.NET gibi çok kanallı kullanıma da uygundur. Her hangi bir çökme veya kilitlenme riski yoktur. Tüm uygulamalarda rahatlıkla kullanılabilinir.

Nasıl Kullanılır?

SQL CE kullanmak için geliştiriciler ekstra hiçbir bilgiye ihtiyaçları yoktur. Mevcut tüm .NET veri erişim metotları ile SQL CE erişimi sağlanabilmektedir. Kod geliştiriciler için en uygun veri tabanıdır. SQL CE veri tabanını kullanmak için önce Visual Studio SP 1 (beta)yı yüklemeniz gerekmektedir. Daha sonra SQL CE Tools For Visual Studio yüklemeniz gerekmektedir.

Örnek Uygulama

Örnek bir Asp.Net adres defteri uygulaması yapalım. Öncelikle boş bir Asp.Net uygulaması açalım.

Uygulamaya SQL CE Veri Tabanı Ekleme

Açtığımız boş uygulamanın App_Data dizinine SQL CE veri tabanını aşağıda ki gibi ekleyelim.

Veri tabanını ekledikten sonra projenin görüntüsü aşağıdaki gibi olacaktır.

Proje referanslarına System.Data.SqlServerCe kütüphanesi ve App_Data dizinine Sql Ce veri dosyasının eklendiğini görüyoruz. Veri tabanı ile çalışmak için gereken kütüphaneyi ve verilerin saklanacağı veri dosyasını projemize eklemiş olduk.

Eklediğimiz Sql Ce dosyasına çift tık ile içini açalım. Şimdi yeni bir tablo ekleyebiliriz.

Eklediğimiz tablolarımızın veri modelini oluşturalım ve daha sonra bu tablolara veri girelim. Örnek adres defteri uygulamamız için People tablosu oluşturdum ve tabloya deneme kayıtlarını girdim.

SQL CE Veri Yapısını Alma

Tüm SQL CE işlemlerimizi tamamladık. Artık uygulama tarafında kullanabiliriz. SQL CE veri tabanına Entity Framework ile bağlanmak için Entity Data Model ekleyelim.

Açılan ekranda “Generate from Database” seçeneğini seçip ilerleyelim. Sonra ki ekranda eklediğimiz AdressBook.sdf dosyasının seçili geldiğini göreceksiniz.

Sırada ki ekranda ekran da SQL CE veri tabanımızda ki tabloların geldiğini görüyoruz. Burada iki tabloyu da seçip sihirbazı tamamlayalım. Oluşan Entity Modeli aşağıda ki gibi olacaktır.

SQL CE ile Çalışmak

Veri tabanında oluşturduğumuz modeli uygulamamıza taşımış olduk. Şimdi veriyi ekranda göstermek gerekiyor. Uygulamanın ana sayfasına bir tane grid kontrolü ekleyelim. Grid kontrolünün DataSourceId özelliğinden “<New Data Source>” seçeneğini seçelim. Karşınıza aşağıda ki veri kaynağı ayarlama ekranı gelecektir.

Ben örnek olarak Entity Framework alt yapısını kullanmak istediğim için Entity seçeneğini işaretledim. Sonra ki sayfada projede ki veri kaynaklarının listesi gelmektedir. Oluşturduğumuz entity data modeli listeden seçelim.

Sonra ki sayfada gride hangi tabloyu bağlamak istediğimizi sormaktadır. People tablosunu seçelim.

Artık uygulamayı çalıştırıp önyüzden kayıtları değiştirebiliriz.

Sonuç

SQL CE kolayca veritabanı depolamasını etkinleştirmek için kullanabileceğiniz ücretsiz, gömülü, veritabanı motoru sağlar.

SQL CE ile yapılan çalışmaları isterseniz kolayca uygulamanın herhangi bir kodu değiştirmek zorunda kalmadan SQL Server veya SQL Azure üzerine taşıyabilirsiniz. Yapmanız gereken sadece uygulama yapılandırma dosyasında <connectionString> değerini değiştirmektir. Böylece uygulama geliştirmek için gerekli küçük gömülü veritabanı çözümü esnekliği sağlanmış olmaktadır.

22 Ocak 2011 Cumartesi

Genişletilebilir Uygulamada Hata Yakalama ve İzleme

Problem

Genişletilebilir (Composite) uygulamalar bir birine bağlı parçalardan oluşmaktadır. Eğer bir parça oluşturulurken parçanın ihtiyaç duyduğu eksik bir bağımlılık var ise uygulama hata alacaktır.

Yukarıda A örneğinin oluşturulması sırasında önce A'nın bağımlılıkları oluşturuluyor. C parçası [Import] edilmiş fakat uygulama içinde C parçasını [Export] eden her hangi bir kontrat bulunmamaktadır. Çalışma anında A parçasını üretmeye çalıştığımızda C'nin [Export] kontratı olmadığından dolayı hata üretilecektir.

Hatalar çalışma anında ortaya çıkacağı için oluşan hataları raporlamalı ve temel sorunu bulmalısınız.

Taşıyıcıyı Yazdırma

Örneğimize geri dönersek A örneği B ile bağımlı ve B ise C ile bağımlı ama uygulamada C örneğini [Export] eden bir kontrat bulunmamaktadır. Yani aşağıd ki şekilde bir kodlama olduğunu varsayalım.

[cs]

[Export ]
 public  class  ClassA 
 {
     [Import ] private  ClassB  _classB ;
 }
 [Export ]
 public  class  ClassB 
 {
     [Import ]
     private  ClassC  _classC ;
 }
 public  class  ClassC 
 {
 }

Taşıcının içeriğini bir stream örneğine yazdırmak için mef.codeplex.com adresinde ki Microsoft.ComponentModel.Composition.Diagnostics.dll assembly dosyasına ihtiyacınız var. Burada yer alan CompositionInfo ve CompositionInfoTextFormatter sınıfları ile taşıyıcının tüm içeriğini bir stream örneğine aktarabilirsiniz.

[cs]

var  compositionInfo = new  CompositionInfo (_aggregateCatalog , _compositionContainer );
 CompositionInfoTextFormatter .Write (compositionInfo, Console .Out );

Yukarıda bir katalog üzerinde ki tüm tanımlamaları çalışma anında Console yazan kod blogu bulunmaktadır. Örneğimie ait çıktı ise aşağıdaki gibi oluşacaktır.

Çıktıyı incelersek katalog üzerinde iki tane [Part] bulunduğunu görüyoruz. A için [Export] tanımının ve [Import] B tanımının olduğunu fakat [Import] B tanımının geçersiz olduğunu görüyoruz. Part B için [Primary Rejection] ifadesini görüyoruz bu ifade bize temel sorunun B parçasında olduğunu gösteriyor. B parçasında da [Import] C tanımını görüyoruz. Hatanın detayını okuduğumuzda [Import] C için geçerli bir [Export] kontratının bulunmadığını görmekteyiz. Böylelikle sorunu tesbilt etmiş olduk. C örneğine ait bir [Export] kontratına ihtiyacımız var.

Sonuç

Böylelikle genişletilebilir (Composite) uygulama yazma konulu serinin sonuna gelmiş olduk. Katalog ve taşıyıcı kavramlarını gördük. [Export] kontratlarımıza nasıl Metadata etiketleri verileceğini ve bu etiketleri kullanarak ihtiyacımız olan parçaları nasıl bulabileceğimizi gördük. Parçaların hayat döngüsünü yönetmeyi inceledik. Son olarakda genişletilebilir uygulamada hata yakalama özelliklerini inceledik.

Genişletilebilir uygulama alt yapısı (System.ComponentModel.Composition) bilinen IoC alt yapılarında (Unity, Structure Map,vb ) farklıdır. IoC alt yapıları derleme anında bağımlılıkları bilmektedir. .NET Composition ile bağımlılıklar çalışma anında keşif edilmektedir. Composition bilinmeyen bağımlılıkları yönetirken IoC ile bilinen bağımlılıklar yönetilmektedir.

Genişletilebilir uygulamalar uzun geliştirme süreci olan projelerde daha çok tercih edilmektedir. Bir sonraki yazı dizisinde genişletileblir MVC 3 uygulaması ve genişletilebilir WCF uygulaması üzerinde durucağız.

21 Ocak 2011 Cuma

Derinlemesine Genişletilebilir Uygulama Yazma

Genişletilebilir uygulamaların asıl gücü tak-çalıştır desteğidir. Bir önce ki yazıda ele aldığımız katalog ve taşıyıcı kavramlarının çalışma anında tak-çalıştır modüllerini nasıl takip ettiğini ve kullandığını inceliyoruz.

Çalışma Zamanı Desteği

Çalışma anında uygulamazın tak-çalıştır modülleri nasıl yönettiğini görelim. Öncelikle katalog ve taşıyıcıyı oluşturalım.

[cs]

var  assemblyCatalog = new  AssemblyCatalog (Assembly .GetExecutingAssembly ());
 var  directoryCatalog = new  DirectoryCatalog (Path .Combine ("../.." , "Plugins" ));
 var  aggregateCatalog = new  AggregateCatalog (assemblyCatalog, directoryCatalog);
 _compositionContainer  = new  CompositionContainer (aggregateCatalog);

DirectoryCatalog örneğinin Plugins dizinini dinlediğini görüyorsunuz. DirectoryCatalog siz yenilemedikçe dizin içinde ki modülleri tekrar yüklemeyecektir. Plugins dizinini dinlemeli ve oluşan değişikliklerde DirectoryCatalog örneğinizi yenilemelisiniz.

[cs]

// Plugins dizinini takip et 
 var  watcher = new  FileSystemWatcher (directoryCatalog.FullPath )
 {
     Filter  = "*.dll" ,
     EnableRaisingEvents  = true 
 };
 // dizindeğişince katalogu yenile
 watcher.Changed  +=  (s, e) => directoryCatalog.Refresh ();

Yukarıda ki kodlama ile Plugins dizininde ki dosyalar değiştiğinde katalogumuzu yeniliyoruz. Katalog ilgili dizinde ki tüm assembly dosyalarına bakarak [Export] tanımlarını almaktadır. Basit bir uygulama ile çalışma anında katalogun nasıl değiştiğini görelim. Elimizde karakter dizisi dönen bir ISeriesFactory interface var.

[cs]

public  interface  ISeriesFactory 
 {
     ///  
     ///   seri oluştur 
     ///  
     /// dizide olması gereken eleman sayısı       /// oluşturdupu seriyi dönderir       List  Create (int  numberOfElement);  }

Bu interface ait uygulama yazmak istiyoruz. Fakat interface uygulama sınıfları çalışma anında eklenecekler. Aşağıdakine benzer bir önyüz hazırlıyoruz. Sol tarafta katalogda yer alan ihraç [Export] tanımları yer alacak. Sağ tarafta ise katalogda yer alan ISeries interface’sini uygulayan sınıflar listelenmiş. Seçilen sınıfı çağıracak olan birde buton eklenmiş durumdadır.

Uygulamayı çalıştırıp uygulamanın dinlediği Plugins dizinine ISeries interface’sini uygulayan assembly dosyalarını kopyalıyoruz. Katalog listesinin doğrudan güncellendiğini görürüz. Bir tane ISeries uygulaması seçip serinin eleman sayısını girelim ve çalıştır diyelim.

ContractName ve MetaData

Uygulama katalogu içinde birden çok ISeriesFactory uygulaması var. Bu durumda elimizde ki parçalar arasında sorgular çalıştırarak istediğimiz parçayı bulmamız gerekmektedir. [ExportMetadata] niteliği ile katalog üzerinde arama yapabilmek için[ Export] tanımlarımıza etiket değerleri verebilmekteyiz. Parçalara ContractName değeri ekleyerek doğrudan istediğimiz parçayı içe aktarabilir veya kendimiz özel sorgular yazarak aradığınız parçayı taşıyıcı üzerinde bulabiliriz.

[cs]

[Export ("Fibanacci" ,typeof  (ISeriesFactory ))]
 public  class  Fibanacci  : ISeriesFactory
 private LazyGetFibanacciSeries(CompositionContainer compositionContainer)
 {
     Lazy  fibanacci =
         compositionContainer.GetExport ("Fibanacci" );
     return  fibanacci;
 }

Veya taşıyıcının sizin yerinize ContractName metadata değeri ile [Import] işlemi yapmasını sağlayabilirsiniz.

[cs]

[Import ("Fibanacci" )]private  ISeriesFactory  _fibanacci ;

Yukarıda ki tanımlama katalog üzerinde aşağıdaki gibi saklanmaktadır.

Görüldüğü üzere yukarıda ki [Export] tanımına ait bir Meradata dizisi bulunmaktadır. [Export] tanımın ilk parametresinde verdiğimiz isim ContractName olarak saklanmaktadır. Metadata dizisinde ExportTypeIdentity etiketinin değeri ise [Export] yapılan tipin tam adıdır. Eğer bir [Export] kontratı için bir isim belirtmez iseniz ExportTypeIdentity değeri ContractName alanına atanacaktır. ContractName ve ExportTypeIdentity metadata değeri ile katalog üzerinde sorgulama yapabilirsiniz.

Buraya kadar her şey güzel ama matematik ve karakter serilerinin aynı listede görülmesini istemiyorsunuz. O zaman kendinize ait [ExportMetadata] etiketleri tanımlama zamanı gelmiştir.

[cs]

[Export ("Fibanacci" ,typeof  (ISeriesFactory ))]
 [ExportMetadata ("Series" ,"Math" )]
 public  class  Fibanacci  : ISeriesFactory
 [Export (typeof  (ISeriesFactory ))]
 [ExportMetadata ("Series" , "String" )]
 public  class  UpperLetters  : ISeriesFactory

Yukarda Prime serisinin bir matematik seri olduğunu benzer şekilde UpperLatter serinin bir karakter serisi olduğunu belirtiyoruz. Şimdi formumuza yeni bir combobox ekleyerek kullanıcıya seri türlerini gösterelim. Her seçilen seri türünde de seri uygulamaları listemizi güncelleyelim.

[cs]

private  void  cbTypes_SelectedIndexChanged (object  sender, EventArgs  e)
 {
     var  selectedType = cbTypes .SelectedItem .ToString ();
     var  seriesList = new  StringList ();
     _compositionContainer 
         .Catalog .Parts 
         .SelectMany (part => part.ExportDefinitions )
         .Where (c=>c.Metadata .ContainsKey ("Series" ) 
                 && c.Metadata ["Series" ].ToString () == selectedType)
         .ToList ()
         .ForEach (c=>seriesList.Add (c.ContractName ));
     sourceSeries .DataSource  = seriesList;
 }

Yukarıda görüldüğü üzere seçilen seri türüne göre seri uygulama sınıflarını listeletebiliyoruz. Fakat bu yöntemde benim eklenti sınıflarında verilen etiket değerlerini bilmem gerekiyor. Metadata nın adı “Series” olmalı değeri “Math” veya “String” olmalı şeklinde kurallarım var. Eklenti kodlanırken bu kurallarımı zorlayan bir şey yok. Benim ihtiyacım ISeriesFactory interface ile birlikte sınıflara Metadata bilgisini de dağıtmalı ve böylece beklenmedik metadata değerleri ile karşılaşmamalıyım. O halde yeni bir Metadata sınıfı yazalım ve bu Metadata ya sahip ISeriesFactory sınıfları ile birlikte çalışalım.

[cs]

public  enum  SeriesType  {Math, String}
 public  interface  ISeriesFactoryMetadata 
 {
     SeriesType  SeriesType  { get ;  }
     string  Name  { get ;  }
 }
 [MetadataAttribute ]
 [AttributeUsage (AttributeTargets.Class,AllowMultiple= false )]
 public  class  SeriesFactoryMetadataAttribute  
     : ExportAttribute , ISeriesFactoryMetadata 
 {
     public  SeriesFactoryMetadataAttribute () 
         : base (typeof (ISeriesFactory )) { }
     public  SeriesType  SeriesType  { get ; set ; }
     public  string  Name  { get ; set ; }
 }

Yukarıda [Export] kontratını türettiğimizi ve sınıfmıza [Metadata] özelliği eklediğimizi görüyorsunuz. Böylelikle [Export] kontratı tanımlarken [Export] ve metadatayı ayrı ayrı tanımlamak zorunda değiliz. Ayrıca Metadata string olarak girilen Metadata etiketlerimizin yerini ISeriesFactoryMetadata interface property'leri aldı. Şimdide uygulamamızda ISeriesFactoryMetadata'yı kullanalım.

[cs]

//[Export("Fibanacci",typeof (ISeriesFactory))] 
 //[ExportMetadata("Series", "Math")] 
 [SeriesFactoryMetadata (Name ="Fibannacci Series" , SeriesType =SeriesType.Math)]
 public  class  Fibanacci  : ISeriesFactory

Görüldüğü üzere kod çok daha anlaşılır oldu. Uygulamayıda ISeriesFactoryMetadata'yı kullanacak şekilde değiştirelim.

[cs]

private  void  btnCreate_Click (object  sender, EventArgs  e)
 {
     var  selectedType = (SeriesType ) cbTypes .SelectedItem ;
     var  selectedSeries = cbSeries .SelectedItem .ToString (); 
     var  series =  _series .First (c => 
         c.Metadata .SeriesType  == selectedType 
         && c.Metadata .Name  == selectedSeries).Value ;
     var  list = new  StringList ();
     list.AddRange (series.Create (int .Parse (txtNumber .Text )));
     sourceResult .DataSource  = list;
 }
 [ImportMany ] private  Lazy [] _series ;
 private  void  cbTypes_SelectedIndexChanged (object  sender, EventArgs  e)
 {
     var  selectedType = (SeriesType ) cbTypes .SelectedItem ;
     var  seriesList = new  StringList ();
     _series 
         .Where (c=>c.Metadata .SeriesType  == selectedType)
         .ToList ()
         .ForEach (c => seriesList.Add (c.Metadata .Name ));
     sourceSeries .DataSource  = seriesList;
 }

Uygulama geliştirici olarak eklentilernden geleçek olan Metadata bilgilerimi güçlü bir tipe çekmiş oldum. Eklenti geliştiricilerede uygulama içinde gerekli olan Metadata bilgilerine doğrudan ulaşmış oldular.

Neden SeriesFactoryMetadataAttribute sınıfnıda bir interface uygulamak zorundadır ? Compositon geri tarafta metadata sınıfı için hala bir anahtar-değer sözlüğü üretmektedir. Doğrudan SeriesFactoryMetadataAttribute sınıfnı [Import] ile almak için SeriesFactoryMetadataAttribute sınıfının üretilen sözlüğü constractor parametresi olarak alması gerekmektedir. Daha kolay yöntem ise SeriesFactoryMetadataAttribute sınıfın bir interface uygulaması ve interface üzerinde ki property değerlerinin composition tarafından okunmasıdır.

Parçaların Hayat Döngüsünü Belirlemek

Ben asal sayıları hesaplayan dizinin her çağrılmada tekrar hesaplama yapmasını istemiyorum. Çünkü bu hesapla uzun süremektedir. Bu sınıfın uygulamada sadece bir defa oluşturulmasını istiyorum. Prime sınıfı ürettiği asay sayıları saklasın ve mümkün oldupunca az hesapla yapsın istiyorum. Taşıyıcıya bu sınıfı her çağrıda oluşturma sadece bir defa oluştur ve kullan demem gerekiyor.

[cs]

[SeriesFactoryMetadata (Name  = "Prime Series" , SeriesType  = SeriesType.Math)]
 [PartCreationPolicy (CreationPolicy.Shared)]
 public  class  Prime  : ISeriesFactory

Böylece Prime sınıfını paylaşımlı/Shared işaretleyerek kullandığım taşıyıcı üzerinde aynı anda sadece bir tane örneği bulunacağını belirtiyoruz. Eğere bir nesnenin her erişimde yeniden oluşturulmasını istiyor iseniz NonShared olarak işaretlemeniz gerekmektedir. Fibanacci serisinde nasıl kullanılması gerektiğini [Import] anında karar verilmesini istiyorum. Yani Fibanacci serisinin [Import] eden parça isterse taşıyıcı üzerinde mevcut olan örneği isterse yeni bir örneği kullanabilir.

[cs]

[SeriesFactoryMetadata (Name  = "Fibannacci Series" , SeriesType=SeriesType.Math)]
 [PartCreationPolicy (CreationPolicy.Any)]
 public  class  Fibanacci  : ISeriesFactory
 [Import("Fibanacci", RequiredCreationPolicy = CreationPolicy.NonShared )]
 private  ISeriesFactory  _fibanacci ;

Composition aksi belirtilmedikce [Export] kontratı olarak Any, [Import] kontratı olrakta Shared kullanmaktadır. Yani aksi belirtilmedikce taşıyıcı üzerinde aynı [Export] kontratı için bir tane parça var olacaktır.

Katalogu Çalışma Zamanında Değiştirme

Fibanacci sınıfın yeni versiyonunu yazdık. Şimdi uygulamayı dinamik olarak güncellememiz gerekiyor. Taşıyıcıya [Import] ettiğiniz parçalarda bir değişiklik olursa tekrar [Import] işlemini yapmasını söyleyebiliyoruz.

[cs]

[ImportMany (AllowRecomposition  = true )] 
private  Lazy [] _series ;

Ayrıca her hangi bir [Import] parçası taşıyan bir sınıfta IPartImportsSatisifiedNotification interface'i uyguladı ise tüm [Import] işlemleri bittikten sonra ImportCompleted olayı taşıyıcı tarafından çağırılmaktadır.

Sonuç

Böylelikle [Export] kontratlarımıza Metadata özelliklerini eklemeyi ve eklediğimiz Metadata özelliklerini bir sınıf yapısına getirmeyi inceledik. Taşıyıcının Metadata ve ContractName ile nasıl çalıştığını ve taşıyıcı üzerinde sorgulama yapmayı gördük. Daha sonra taşıyıcı üzerinde ki parçaların hayat döngüsünü yönetmeyi inceledik. Son olarakta çalışma anında değişen katalogdan haberdar olmayı gördük. Bu iki yazı ile her hangi bir .NET platformunda genişletilebilir uygulama yazacak duruma geldik. Bir sonra ki makalede Genişletilebilir Uygulamalarda Hata Ayıklama ve Takip etme özelliklerini inceliyor olacağız.

19 Ocak 2011 Çarşamba

NuGet:Microsoft Ve Open Source

NuGet (http://nuget.org/)  açık kaynaklı projeleri kolaylıkla bulup kendi projenize eklemeyi sağlayan MS tarafından (açık kaynak kodlu olarak) geliştirilen Visual Studio eklentisidir. NuGet tüm geliştiricilere bir online galeri sağlar. Geliştiriciler bu online galeriden istedikleri açık kaynak kodulu projeyi kendi projelerine referans olarak dahil edebilmektedir. Ayrıca NuGet tüm VS proje tipleri ile birlikte çalışabilmektedir.

Açık kaynak kodlu projeler kullanırken en büyük sıkıntı projeler arasında ki bağımlılıkları yönetmektir. NuGet proje bağımlılıklarını kendisi yönetmektedir. Aynı zamanda NuGet kullanıdığınız açık kaynak kodlu proje için gerekli web.config düzenlemesinide yapmaktadır. Kullandığınız projenin yeni versiyonu çıktığında yükseltme işinide kolaylıkla yapabilmektesiniz. Eğer kendi geliştirdiğiniz bir açık kaynak kodlu projeniz var ise galeriye ekleyip diğer geliştiricilerin kullanımınada sunabilirsiniz.

Kısacası NuGet ile amaç açık kaynak kodlu projeleri mümkün olduğu kadar kolay şekilde .NET projelerine dahil etmektir.

Nasıl Kullanılır ?

NuGet eklentisini kurdunuz ve çalışmaya hazırsınız. Her hangi bir proje üzerinde sağ tıkladığınızda paket ekleme menüsünün geldiğini görürsünüz.

Açılan formdan projenize dahil ettiğiniz paketleri ve online katalog üzerinde yer alan tüm açık kaynak kodlu projleri görebilirsiniz. Ayrıca  projenizde bulunan güncellenebilir durumda ki tüm paketleri de açılan formdan görebilmektesiniz. Ben bu örnekte online katalog üzerinde Moq projesini aradım. Moq projesini kendi projeme dahil ettim.

Moq projesini dahil ettikten sonra proje referanslarına Moq.dll geldiğini ve package.config dosyasınında projeye eklendiğini göreceksiniz.

NuGet ile eklediğiniz açık kaynak kodlu projeyi kaldırmak istediğinizde tekrar paket ekleme formuna gidip referansı projenizden kaldırabilirsiniz. Aynı zamanda NuGet Package Manager Console (View -> Other Windows -> Package Manager Console ) ile de paketleri yönetebilirsiniz.

Sonuç olarak NuGet ve Visual Studio ile açık kaynak kodlu projelerin kullanılması ve dağıtılması son derece kolay hale geldi.

14 Ocak 2011 Cuma

Genişletilebilir Uygulama Yazma

Uygulamaların hem fonksiyonel hem de ara yüz olarak genişletilebilir olması bir ihtiyaçtır. Fakat genelde uygulamalar tek parça halinde yazılırlar ve tak-çalıştır mantığına sahip bir genişletilebilme yeteneği sunmazlar. System.ComponentModel.Composition isim uzayında bu genişletilebilirlik problemi için isteyen her uygulamanın kullanabileceği basit bir genişletilebilir uygulama çözüm alt yapısı sunulmaktadır. Alt yapı çalışma anında uygun genişleletme parçalarını bulur ve yükler. Ayrıca alt yapı yüklenen parçaların yaşam döngülerini de takip eder.

System.ComponentModel.Composition ile birlikte uygulamalar için Katalog (Catalog) ve taşıyıcı (Container) kavramları gelmektedir.

Katalog sadece uygulama genelinde ki dağıtım(Export) tanımlarını saklamaktadır. Yani uygulamanın ihtiyaç duyduğu bir nesnenin nereden ve nasıl oluşturulacağının cevabını katalog vermektedir. Katalog çalışma zamanında kendisini güncelleyen bir yapıdadır. Belirtilen dizinde ki dll’ler içinden dağıtım tanımlarını toplayabilmektedir. Dizin içinde ki dll’ler değiştikçe katalogda kendisini güncellemektedir.

Taşıyıcı ise parçaları(Part) bir araya getiren kısımdır. Her bir parça kodlanırken uygulama geneline ithal etmek istediği (Export) tanımları ve uygulamadan almak istediği (Import) ihraç tanımları söylemektedir. Taşıyıcı çalışma anında parçaları oluşturur ve kullanımda olduğu sürece saklar.


Taşıyıcı ve katalog arasında çalışan fakat bizim görmediğimiz ExportProvider sınıfı vardır. ExportProvider sınıfı parçaları oluşturan sınıftır.Taşıyıcı ve katalog ile konuşarak kendisinden istenen parçayı üretmektedir.

Ortamı Hazırlama

Katalog Oluşturma

Uygulama ilk olarak tip tanımlarını saklayacağı bir tip katalog oluşturmalıdır. En sık kullanılan kataloglardan ilk AssemblyCatalog’dur. AssemblyCatalog verdiğiniz assembly dosyasını taramakta ve parça kontratlarını saklamaktadır.

[cs]

var  assemblyCatalog = new  AssemblyCatalog (Assembly.GetExecutingAssembly ());

Başka bir katalog türü ise DirectoryCatalog’dur. DirectoryCatalog ise verilen dizinde ki tüm assembly dosyalarını taramakta ve bulduğu tüm parça taşıma kontratlarını saklamaktadır.

[cs]

var  directoryCatalog = new  DirectoryCatalog (Path .Combine (".." , "Plugin" ));

Birden çok katalogu AggregateCatalog ile bir arada kullanarak daha geniş bir parça taşıma kontratı havuzuna sahip olabilirsiniz.

[cs]

var  aggregateCatalog = new  AggregateCatalog (assemblyCatalog, directoryCatalog);

Bu üç katalog dışında daha az kullanılan SilverLight’a özel DeploymentCatalog ve sadece parametre olarak verilen nesneler için tip katalogu oluşturan TypeCatalog sınıfları da mevcuttur.

Taşıyıcı Oluşturmak

Parçalarımızın program içinde nasıl hareket edeceğini gösteren kontratlarımızı barındıran katalogumuzu oluşturduk. Şimdi sıra katalogu kullanacak olan taşıyıcıyı oluşturmaktadır. Taşıyıcıyı iki şekilde oluşturabilirsiniz. Eğer taşıyıcıya hiçbir katalog vermeden oluşturursanız taşıyıcı varsayılan olarak katalogsuz olarak oluşturacaktır. Fakat katalog olmadığı için parçaları oluştururken gerekli tüm parçaları doğrudan taşıyıcıya eklemeniz gerekecektir. Burada dikkat edilmesi gereken husus taşıyıcı [Export] [Import] niteliklerini sahip parçalar ile çalışabilmektedir.

[cs]

var  compositionContainer = new  CompositionContainer ();

Katalogsuz taşıyıcı kullanma durumu hemen hemen hiç kullanılmamaktadır. Genelde katalogsuz olarak taşıyıcı oluşturulmamaktadır. Eğer kendinize ait bir katalog oluşturdunuzsa taşıyıcının bu katalog ile çalışmasını isteyebilirsiniz.

[cs]

var  aggregateCatalog = new  AggregateCatalog (assemblyCatalog, directoryCatalog);
 var  compositionContainer = new  CompositionContainer (aggregateCatalog);

Parçalar ve Kontratlar

Katalog ve taşıyıcıyı oluşturduktan sonra sıra uygulama parçalarını oluşturmaya geldi. [Export] ve [Import] niteliğine sahip tüm sınıflar, ara yüzler, değişkenler, metotlar program parçası sayılmaktadır. [Export] ve [Import] nitelikleri ile parçalar taşıyıcıya uygulama içinde nasıl hareket edeceğini yani taşınma kontratlarını belirtmektedir.

Dışa Aktarım

Her hangi bir parçaya sadece [Export] niteliği ekleyerek dışa aktarılabilir olmasını sağlayabilirsiniz.

[cs]

// 1- Basit dışa aktarım
 [Export ]
 public  class  SimpleContractExport

Sınıfınızı üst sınıfın bir örneği olarak da dışa aktarabilirsiniz.

[cs]

// 2- üst sınıfı ile dışa aktarım kontratı 
 [Export (typeof (SimpleContractExport ))]
 public  class  SimpleDrivenTypeExport  : SimpleContractExport

Sınıfınızı özel bir etiket belirterek de dışarı aktarabilirsiniz. Bu durumda aynı etiket ile çağrıldığında bu dışa aktarım kontratı çalışacaktır.

[cs]

// 3- etiket ile dışa aktır kontratı
 [Export ("SimpleTagedTest" )]
 public  class  SimpleTagedTypeExport

Dışarı aktarmak istediğiniz bir ara yüzün uygulaması da olabilmektedir.

[cs]

// 4- bir arayüz uygulması olarak dışa aktarma
 [Export (typeof (ISimpleInterface ))]
 public  class  InterfaceContractExport  : ISimpleInterface

Her zaman bir sınıfı aktarmak zorunda değilsiniz. Diğer program bileşenleri de dışarı aktarılabilmektedir. Her hangi bir property dışarı aktarılabilmektedir.

[cs]

// 5- bir property olarak dışa aktarım kontratı
 [Export ("NextString" )]
 public  string  NextStringProperty

Yukarıdaki örnekte bir etiket verilmiştir çünkü bir string tipi dışarı aktarılmaktadır. Birçok string tipi arasında bulabilmek için bir etiket verilmiştir. Genelde property olarak aktarımlar Factory Pattern uygulamak için kullanılmaktadır.

[cs]

// 5- bir property olarak dışa aktarım kontratı
 [Export ]
 public  ISimpleInterface  SimpleInterfaceFactory 
 {
     get 
     {
         return  new  OtherInterfaceContractExport ();
     }
 }

Tam bu noktada nesnelere police enjekte etmek için ara bir çözüm bulunmaktadır. Eğer uygulamanızda ki sadece birkaç nesne üzerinde “Policy Injection” kullanıcaksanız property olarak aktarım işinize yarayacaktır.

[cs]

// 5- bir property olarak dışarı aktarım  
 // ve özel bir sınıfta policy injection kullanımı
 [Export ]
 public  ISimpleInterface  SimpleInterfaceFactoryWithPI 
 {
     get 
     {
         return  RoC.PI.ProxyCreator
             .CreateObject ();
     }
 }

Sadece property’leri değil event’larıda dışarı aktarabilmektesiniz.

[cs]

// 6- bir metod olarak dışa aktarım kontratı
 [Export (typeof (Func))]
 public  string  GetNext ()

Olayları uygulama genelinde paylaşırken dikkatli olmak gerekmektedir. Zira bir olayı dışarı aktarmak için taşıyıcı önce olayı barındıran sınıfı oluşturacaktır. Olayı barındıran sınıf olaya bağlanan tüm delegeler yok olduğunda yok edilecektir. Fakat olaya bağlanan delegelerin olduğu sınıflarda yok edilmek içinde delegelerin yok edilmesini bekleyecektir. Yani uygulamanızda gereksiz yere bir birini bekleyen nesneleriniz kalacak ve bunlar yok edilmeyecektir. Bu durumda kullanılacak WeakReference , WeakEventPattern ve EventAggregator gibi yöntemler bulunmaktadır. Başka bir yazıda açıklamak üzere uygulama genelinde olay paylaşımının tehlikesinden bahsedip geçmiş olalım.

Bazen de bir üst sınıf veya ara yüze ait tüm alt sınıfların dışarı aktarılabilir olmasını istersiniz. Bu durumda üst sınıfınızı işaretleyerek tüm alt sınıflarınızın taşınabilir olmasını sağlayabilirsiniz.

[cs]

// 7- bir üst sınıf olarak dışa aktarım kontratı
 [InheritedExport ]
 public  interface  INext { .. }
 public  class  NextClass1  : INext { .. }
 public  class  NextClass2  : INext{ .. }

Böylelikle kullanabileceğimiz dışa aktarım kontratlarını inceledik. Hafızada yer tutan tüm programlama öğelerini dışarı aktarabiliriz. Aktarırken bir etiket verebilir veya program öğesinin tipi ile değil de dönüşebileceği her hangi bir tip ile aktarabiliriz.

İçe Aktarım

Tabii ki dışarı aktardığımız program parçalarının diğer program parçaları tarafından içeri aktarılması gerekmektedir. Dışa aktarıma benzer şekilde içe aktarım yolları vardır.

Nesnenin oluşması sırasında Cunstructor parametresi olarak diğer parçaları ithal edebilirsiniz. Aşağıdaki örnekte SimpleImport nesnesi oluşturulurken taşıyıcı InterfaceContractExport tipine ait bir dışa aktarım kontratı olup olmadığına bakar. Bulduğu InterfaceContractExport kontratı ile eğer taşıyıcı üzerinde nesne örneği yoksa önce aktaracağı InterfaceContractExport nesnesini oluşturur. Taşıyıcıda içe aktarılacak InterfaceContractExport nesne örneğini alır ve SimpleImport sınıfına constructor parametresi olarak gönderir.

[cs]

// 1- constractor parametresi olarak kontratları içe aktarım
[ImportingConstructor ]
public  SimpleImport (InterfaceContractExport  simpleInterfaceImplemantation)

Her zaman ithal etmek istediğiniz parça için bir dışa aktarım kontratı olmayabilir. Eğer ithal etmek istediğiniz parça var ise parçayı ithal etmeniz gerekmektedir.

[cs]

// 2- opsiyonel içe aktarı 
 // eger import edilen parça var ise getir yoksa Default(T) değerini getir 
 [ImportingConstructor ]
 public  SimpleImport ([Import (AllowDefault  = true )]INext  next)

İçe aktarımı property olarak da kullanabilirsiniz. Nesne create edildikten sonra property aktarımları atanacaktır.

[cs]

// 3-  Property olarak kontratları içe aktarım
 [Import ]
 public  SimpleContractExport  Simple  { get ; set ; }

Property içe aktarımla aynı şekilde değişkenlerde de içe aktarım yapılabilmektedir.

[cs]

// 4. Field olarak içe aktarım 
  // etiketleri tüm aktarımda kullanabilirsiniz
 [Import ("SimpleTagedTest" )]
 private  SimpleTagedTypeExport  _simpleTagedTypeExport ;

Eğer içe aktarmak istediğiniz parça taşıyıcı üzerinde birden çok varsa bunların hepsini birden alabilirsiniz. Bir sonra ki yazıda taşıyıcı üzerinde ki parçalar üzerinde sorgular çalıştırıp istediğiniz özel parçaları bulmayı inceleyeceğiz. Şimdilik belirli bir tipte ki tüm parçaları alalım.

[cs]

// 5 -kontratları çoklu içe aktarım
 [ImportMany ]
 private  INext [] _nextArray ;

Bu şekilde parça paylaşan programlarda en büyük ihtiyaçlardan biriside her hangi bir parçanın ilk erişildiğinde oluşturulmasıdır. Yani içe aktardığınız parçaya ait bir metot veya property erişimi yapana kadar parça oluşturulmayacaktır. Parçayı kullanan sınıf ilk ne zaman erişim sağlarsa o zaman içe aktarılan parça oluşturulacaktır.

[cs]

// 6 -kontralları gevşek bağlama (lazyload ) ile içe aktarım
 // ilk erişeme kadar nesneyi taşıyıcı üzerinden almaz.
 // ilk erişimde nesne taşıyıcıdan getirilir
 [Import ]
 public  Lazy  SimpleLazy  { get ; set ; }
 public  void  UseLazy ()
 {
     // SimpleLazy.IsValueCreated  == false
 
     // ilk erişimi yapılıyor bu kod satırı çalışana
     // ISimpleInterface nesne örneği oluşturulmuyor
     SimpleLazy .Value .GetExecutingAssemblyName ();
 
     // SimpleLazy.IsValueCreated  == true 
 }

Basit Genişletilebilir Uygulama

Basit bir uygulama ile genişletilebilir (composite) özelliklerini test edelim. Öncelikle katalogu olmayan bir taşıyıcıyı nasıl kullanacağımızı görelim:

[cs]

[Export ]
 public  class  ParameterTest1 
 {
     public  int  Number1 { get  { return  10; } }
 }
 public  class  ParameterTest2 
 {
     [Export ("ParameterTestNumber" )]
     public  int  Number2  { get  { return  11; } }
 }
 public  class  ContainerWithOutCatalog 
 {
     public  ContainerWithOutCatalog ()
     {
         // katalog olmadan taşıyıcı oluştur
         var  container = new  CompositionContainer ();
         // this içinde ki [Import] ları parametrelerle doldur
         container.ComposeParts (this ,new  ParameterTest1 (), new  ParameterTest2 ());
         Console .WriteLine (
             "ContainerWithOutCatalog.ParameterTest.Number1:{0}" ,
             ParameterTest .Number1 );
         Console .WriteLine (
             "ContainerWithOutCatalog._numberTest:{0}" ,
             _numberTest );
     }
     [Import ]
     public  ParameterTest1  ParameterTest  { get ; set ; }
     [Import ("ParameterTestNumber" )] private  int  _numberTest ;
 }

Yukarıda ki kodlar sorunsuz şekilde çalışmaktadır. Dikkat edilmesi gereken [Import] yapılan tüm parçaların ComposeParts fonsiyonuna parametre olarak vermek durumundasınız. Çünkü gerekli parçalara ait [Export] kontratlarını alabilecek bir kataloga sahip degilsiniz.

Kataloga sahip bir taşıyıcı ile çalışmayı görelim:

[cs]

public  class  ContainerWithCatalog 
 {
 public  ContainerWithCatalog ()
 {
     var  assemblyCatalog = new  AssemblyCatalog (GetType ().Assembly );
     var  compositionContainer = new  CompositionContainer (assemblyCatalog);
     compositionContainer.ComposeParts (this );
     //üüm [Export] ve [Import]ıı y dene 
     Console .WriteLine ("ContainerWithCatalog.ImportSimple:{0}" , 
         _simpleImport .SimpleContractImport .Number ());
     Console .WriteLine (
         "ContainerWithCatalog.ImportAllowDefault:{0}" ,
         _simpleImport .NonExported  == default (INext ) ? "default"  : "non default" );
     Console .WriteLine ("ContainerWithCatalog.ImportDrivenType:{0}" ,
         _simpleImport .DrivenTypeImport .Number ());
     Console .WriteLine (
         "ContainerWithCatalog.ImportWithLabel:{0}" ,
         _simpleImport .GetSimpleTagedTypeExport2 ());
     Console .WriteLine (
         "ContainerWithCatalog.ImportingConstructor:{0}" ,
         _simpleImport .SimpleInterface .Number ());
     Console .WriteLine (
         "ContainerWithCatalog.ImportProperty:{0}" ,
         _simpleImport .NextStringProperty );
     Console .WriteLine (
         "ContainerWithCatalog.ImportEvent:{0}" ,
         _simpleImport .NextNumber .Invoke ());
     Console .WriteLine (
         "ContainerWithCatalog.ImportMany:{0}" ,
         _simpleImport .WriteAllNext ());
 }
 [Import ] private  SimpleImport  _simpleImport ;
 }

Uygulamanın çıktısı şu şekilde görülecektir:

 Temel seviyede genişletilebilir bir uygulama geliştirebilir duruma geldik. Artık özgürüz! Kendimiz için bir katalog oluşturabiliriz. Oluştuğumuz katalogu kullanarak uygulama parçalarını bir araya getirecek taşıyıcıyı oluşturabiliriz. Taşıyıcı uygulama parçalarımızı bizim belirlediğimiz kontratlar ile uygulama içinde yönetecektir. Bir sonra ki yazıda katalog üzerindeki kontratları nasıl sorgulayacağımızı, parçalarımızın hayat döngüsünü, taşıyıcıyı ve katalogu çalışma anında değiştirmeyi inceleceğiz.

4 Ocak 2011 Salı

150 satırda Policy Injection

Programlamada kodlar uzadıkça kodların anlaşılabilirliği düşmektedir ve çoğu zamanda içinden çıkılmaz bir hal almaktadır. Tek bir "basit" kayıt işlemini yapan kod bloğu bile tüm uygulamada geçerli olan hata yakalama, loglama, yetki kontrolü gibi çapraz modüllere ait kodlardan dolayı "karmaşıktır". Örneğim aşağıda bu "basit" kayıt işlemine bir örnek verilmiştir.

[cs]

public  void  SaveOrderToDB (Order  order, User  user)
 {
     LogManager .BeginPerformanceCounter ("SaveOrderToDB" );
     if  (order == null  || user == null )
         throw  new  NullReferenceException ();
     if  (!user.IsInRole ("Sales" ))
         throw  new  SecurityException ();
     var  totalAmount = order.Unit *order.UnitAmount ;
     var  balance = GetBalanceFormAccount (order.AccountId );
     if  (totalAmount > balance)
         throw  new  ApplicationException ();
     try 
     {
         var  dbContext = new  DbContext ();
         dbContext.Orders .Add (order);
         dbContext.SaveChanges ();
     }
     catch  (Exception  exception)
     {
         LogManager .EndPerformanceCounter ("SaveOrderToDB" );
         LogManager .SaveException (exception);
         throw ;
     }
     LogManager .EndPerformanceCounter ("SaveOrderToDB" );
 }

Burada görüldüğü üzere müşterinin hesabından yapılan basit bir ödeme işleminde bile kodu karmaşık hale getiren bir çok çapraz modül söz konusudur. Öncelikle fonksiyona girerken loglama modlü çalıştırılıyor sonra parametrelerin doğrulanması daha sonra işlemi yapan kullanıcının yetki doğrulaması yapılıyor. Daha sonra hata kontrolleri ve en son fonksiyondan cıkmadan önce tekrar loglama yapılıyor. Aslında yapılmak istenen sadece veri tabanına doğru bir kayıt eklemektir. Buda 26 satırlık fonksyonumuzda ki 9 satırda ya yapılmaktadır. Poliçe enjekte veya Cephe Yönelimli (Asspect Oriented) programlama yaklaşımı bu çapraz modülleri mümkün olduğunca bir noktadan yönetilmesini ve fonksiyonun içinde ki kod bloğundan uzaklaştırılmasını amaçlamaktadır. Eğer bu fonksiyonu Policy Injection kullanarak yazmış olsaydık aşağıdaki şekilde görülecekti:

[cs]

[PerformanceCounterHandler ]
 [ExceptionHandler ]
 [CheckUserIsInRoleHandler ("Sales" )]
 [CheckArgumentsHandler ]
 public  void  SaveOrderToDB (Order  order, User  user)
 {
     var  totalAmount = order.Unit  * order.UnitAmount ;
     var  balance = GetBalanceFormAccount (order.AccountId );
     if  (totalAmount > balance)
         throw  new  ApplicationException ();
     var  dbContext = new  DbContext ();
     dbContext.Orders .Add (order);
     dbContext.SaveChanges ();            
 }

Görüldüğü üzere kodlama daha temiz ve sade oldu. Bu fonksiyonda tüm gerekli çapraz modüller fonksiyona bir poliçe olarak eklenmektedir. Fonksiyona yapılan çağrıda resimde görüldüğü şekilde bir sorumluluk zinciri takip edilmektedir. Önce Performans counter modülü sonra sırası ile diğer modüller çalışacak en son metot çalışacaktır. Daha sonra sonuç aynı yolu izleyerek geri döndürülecektir. Bu yapıya sorumluluk zinciri denmektedir. Yukarıda ki örneğimizde sorumluluk zinciri şu şekilde çalışacaktır: Performans Counter -> Exception Handler -> Sheck User Is In Role -> CheckArguments -> Metot -> Check Arguments -> Check User Is In Role -> Exception Handler -> Performans Counter. Bu sıralama sizi korkutmasın performans olarak kaybınız mili saniyelerin altındadır. Kod sadeliği ve yönetim kolaylığı olarak kazançınız çok daha fazla olacaktır.

Peki projemizde ki çapraz modülleri fonksiyonlarımıza birer poliçe olarak nasıl enjekte edebiliriz ?

Her şeyden önce poliçe enjekte edebilmek için nesnenin her hangi bir metotu çağrıldığında tetiklenecek bir yönetim fonksiyonuna ihtiyaçımız var. Yani sınıfa ait property'ler değiştiği zaman fırlatılan PropertyOnChanged olayına benzer, fonksiyon çağrılarında tetiklenecek bir mekanizmaya ihtiyacımız var. İhtiyaçımıza proxy sınıfı cevap vermektedi. Proxy sınıfı içlerinde gerçek nesneyi barındıran gerçek nesneye yapılan her çağrıyı izleyen sınıflardır. Proxy içende barındırdığı nesneye bir çağrı geldiğinde Invoke fonksiyonu tetiklenmekte ve gerçek nesneye ait çağrı bu fonksiyon içinde yapılmaktadır. Proxy'ler yardımı ile nesneye gelen metot çağrılarını tek bir yerden kontrol edebiliriz. Fakat nesneye bir proxy oluşturabilmek için nesnelerimizi MarshalByRefObject sınıfından türetmeliyiz. Böylece proxy metot çağrılarını takip edebilecektir.

[cs]

public  class  PITest2 : MarshalByRefObject 
 {
     [PerformanceCounterHandler ]
     [ExceptionHandler ]
     [CheckUserInRoleHandler ("Sales" )]
     [CheckArgumentsHandler ]
     public  void  SaveOrderToDB (Order  order, User  user)
     {

Nesnemizi proxy oluşturabilecek duruma getirdik.

Sonraki problemimiz çapraz modülleri merkezi bir noktaya çekmektir. Çapraz modüllerin her birini bir poliçe haline getirebiliriz. Policeden kasıt çalıştığı an ki ortam bilgilerini toplayan ve tek bir atomik iş yapan uygulama bloğu sınıflarıdır. Örneğim hata yakalama işlemi için uygulamada bulunan tüm fonksiyonlarda çalışacak bir ExceptionHandlingManager yazabilirsiniz. Bu durumda her bir fonksiyonun içinde ExceptionHandlingManager sınıfınıza en az bir defa çağrıda bulunmanız gerkecektir. Yada hata kayalama işlemini yapan tek bir police yapar ve hata yakalama gereksimi olan fonksiyonların bu police ile çalışmasını sağlayabilirsiniz. Böylece hata yakalama sisteminizde bir değişiklik yapmak için sadece poliçenizi değiştirmeniz yeterli olacaktır. Ayrıca fonksiyonların içinde ki gereksiz kod bloklarından kurtulmuş olacaksınız.

[cs]

public  class  ExceptionHandlingCallHandler  : CallHandler 
 {
 public  override  ReturnMessage  Invoke (IMethodCallMessage  message, object  realObject)
 {
     var  result = NextHandler .Invoke (message, realObject);
     if  (result.Exception  != null )
     {
         var  errorMessage = string .Format ("{0}{1}{2}" , DateTime .Now , result.Exception .Message ,
                                             result.Exception .StackTrace );
         Console .WriteLine (errorMessage, ConsoleColor.Red, true );
         Trace .TraceError (errorMessage);
     }
     return  result;
 }
 }

Yukarıda örnek bir hata yakalama policesini görmektesiniz. Police sorumluluk zincirinde yer alan kendsinden sonra ki policeleri çağırmakta eğer bu zincir üzerinde bir hata oluşursa bunu ekrana basmaktadır. Şimdi nesnemizin nasıl kullanıldığına bakalım:

[cs]

PITest2  instance = PolicyInjector .CreateObject ();
 var  order = new  Order  {AccountId  = 10, Unit  = 3, UnitAmount  = 5};
 var  user = new  User  {UserName  = "saleperson1" , Roles  = new [] {"Sales" }};
 instance.SaveOrderToDB (order,user);

Kullanımda ki tek fark nesneyi doğrudan oluşturmak yerine PolicyInjector Factory sınıfı ile oluşturmamızdır.

Şimdi PolicyInjector sınıfını inceleyelim. Nesnelere poliçeleri enjekte edebilmek için nesnelerin tek bir merkezden üretilmesi gerekmektedir. PolicyInjector bizim yerimize sınıflarımızın proxy'lerini oluşturan, metot çağrılarında ki sorumluluk zincirini oluşturan ve metot çağrısı sırasında poliçeleri enjecte eden Factory sınıfımızdır. Yaptığı iş çok sihirlide olsa kendisi oldukça basit ve 150 satırlık bir sınıfdır.

[cs]

public  class  PolicyInjector  : RealProxy  where  TObject  : new ()
 {
     private  readonly  TObject  _realObject ;
     public  PolicyInjector (TObject  realObject)
         : base (typeof (TObject ))
     {
         _realObject  = realObject;
     }
     public  static  TObject  CreateObject ()
     {
         var  reelInstance = new  TObject ();
         return  Wrap (reelInstance);
     }
     public  static  TObject  Wrap (TObject  reelInstance)
     {
         var  proxyObject = new  PolicyInjector (reelInstance);
         var  transparentProxyObject = (TObject )proxyObject.GetTransparentProxy ();
         return  transparentProxyObject;
     }
     public  override  IMessage  Invoke (IMessage  msg)
     {
         var  message = (IMethodCallMessage )msg;
         var  chain = CreateChainOfResponsibility (message);
         var  result = chain.Invoke (message, _realObject );
         return  result;
     }
     private  CallHandler  CreateChainOfResponsibility (IMethodMessage  message)
     {
         var  chain = new  List ();
         AddToChainFromInterfaceHandlers (message, chain);
         AddToChainFromClassHandlers (message, chain);
         // add method call 
         chain.Add (new  PolicyPear  { CallHandler  = new  MethodCallHandler () });
         // organize chain 
         for  (var  i  = 1; i < chain.Count; i++)
         {
             chain[i-1].CallHandler.SetChain(next:chain[ i ].CallHandler, 
                 handlerAttribute: chain[i - 1].HandlerAttribute );
         }
         return  chain[0].CallHandler ;
     }
     private  void  AddToChainFromInterfaceHandlers (IMethodMessage  message, ICollection  chain)
     {
         var  realType = _realObject .GetType ();
         foreach  (var attributes  in 
             realType.GetInterfaces ()
             .SelectMany (@interface => (
                 from  method in  realType.GetInterfaceMap (@interface).InterfaceMethods 
                 where  method.Name  == message.MethodBase .Name 
                 select  method.GetCustomAttributes (typeof (CallHandlerAttribute ), true ))))
         {
              attributes.Cast ()
                 .OrderBy (c => c.CallHandlerType .MetadataToken ).ToList ()
                 .ForEach (attribute =>
                     chain.Add (new  PolicyPear 
                     {
                         CallHandler=Activator.CreateInstance(attribute.CallHandlerType) as CallHandler,
                         HandlerAttribute  = attribute
                     })
                 );
         }
     }
     private  void  AddToChainFromClassHandlers (IMethodMessage  message, ICollection  chain)
     {
         var  realType = _realObject .GetType ();
         var  attributes =
             realType.GetMethod (message.MethodName ).GetCustomAttributes (typeof (CallHandlerAttribute ), true );
         if  ((attributes == null ) || (attributes.Length  <= 0)) return ;
         bool  needReserve = chain.Count  == 0;
         var  list = attributes.Cast ()
             .ToList ();
         if  (needReserve) list.Reverse ();
         list.ForEach (attribute => chain.Add (new  PolicyPear 
         {
             CallHandler  = Activator .CreateInstance (attribute.CallHandlerType ) as  CallHandler ,
             HandlerAttribute  = attribute
         }));
     }
     private  sealed  class  MethodCallHandler  : CallHandler 
     {
         public  override  ReturnMessage  Invoke (IMethodCallMessage  message, object  realObject)
         {
             var  methodRetval = message.MethodBase .Invoke (realObject, message.InArgs );
             return  new  ReturnMessage (methodRetval, null , 0, message.LogicalCallContext , message);
         }
     }
     private  sealed  class  PolicyPear 
     {
         public  CallHandler  CallHandler  { get ; set ; }
         public  CallHandlerAttribute  HandlerAttribute  { get ; set ; }
     }
 }
 public  abstract  class  CallHandler 
 {
     internal  void  SetChain (CallHandler  next, CallHandlerAttribute  handlerAttribute)
     {
         NextHandler  = next;
         CallHandlerAttribute  = handlerAttribute;
     }
     public  CallHandler  NextHandler  { get ; private  set ; }
     protected  CallHandlerAttribute  CallHandlerAttribute  { get ; private  set ; }
     public  abstract  ReturnMessage  Invoke (IMethodCallMessage  message, object  realObject);
 }
 [AttributeUsage (AttributeTargets.Method  )]
 public  class  CallHandlerAttribute  : Attribute 
 {
     public  CallHandlerAttribute (Type  callHandlerType)
     {
         if  (callHandlerType !=  null  && !typeof (CallHandler ).IsAssignableFrom (callHandlerType))
             throw  new  TypeLoadException (string .Format ("{0} is not assingable from {1}" , callHandlerType.Name ,
                                                         typeof (CallHandler ).Name ));
         CallHandlerType  = callHandlerType;
     }
     public  Type  CallHandlerType  { get ; private  set ; }
 }

Tüm bu sihirli işleri yapan kodların hepsi bu kadar!

PolicyInjector yeni bir nesne oluştururken nesnenin proxy örneğini geri döndermektedir. Proxy örneği üzerinde yapılan tüm metot çağrıları PolicyInjector sınıfımızın Invoke metoduna düşmektedir. Invoke metotu içinde önce metotun poliçelerine göre bir sorumluluk zinciri oluşturulmaktadır. Sorumluluk zinciri oluşturulurken önce interface üzerinde ki metot tamına ait poliçeler zincire eklenmektedir. Daha sonra metota ait poliçelerde zincire eklenmektedir. Zincir oluşturulurken her bir poliçeye bir sonra ki poliçe atanmaktadır. Zincir oluşturulduktan sonra zincirin ilk elemanı çağrılmaktadır. Zincir üzerinde ki her bir police kendi işlevini yapmakta ve bir sonraki policeyi çağırmaktadır. Sorumluluk zincir üzerinde ilerlemekte ve en son metodu çağırmaktadır. Metot çalıştıktan sonra sorumluluk zincir üzerinde sondan başa doğru hareket etmekte ve PolicyInjector Invoke metotu içinde ki result değişkenine sonuç atanmaktadır. Invoke fonksiyonu da bu sonucu metodu çağıran koda dönmektedir.

Böylelikle 150 satırda projenizde ki karmaşayı ortadan kaldıraçak bir PolicyInjector sınıfına ulaşmış olduk. Policy Injection metotu ile çapraz modülleri policeler haline getirip çok ciddi bir kod temizliği ve yönetim kolaylığı sağlayabilirsiniz.

Yardımlarından dolayı Engin Özer Beye teşekkürler...