28 Mart 2011 Pazartesi

[Yazılım Felsefesi] Asp.Net'de MVVM'i Denemek

Merhaba Arkadaşlar;

Bu makalenin kaynak kodlarını CodePlex'deki Adresinden (AspNetMVVM_TryOut.rar) indirebilirsiniz.

Bu yazı fikirsel çalışmalarımı yayınladığım "[Yazılım Fesefesi] UI Pattern'ler ve MVVM Üzerine Fikirsel Çalışmalar" makalemin uygulama örneğidir.

Önceki yazıda fikirler ortaya atmıştık. Sonra hayal ettim, bir şeyler ortaya çıkarttım. Sizinle paylaşmak istedim.
Asp.Net üzerinde MVVM denemeleri yapmaktaki öncelikli amacım; Code-Behind kodlamadan kurtulmaya ve UI Bussiness'ı bir tür model'e almaya çalışmaktı. Bunu kısmen gerçeklediğimi söyleyebilirim. Peki bu söylediğim şeyi Asp.Net MVC yapmıyor mu? Evet bir kısmını yapıyor. Controller sınıfları, bir tür Façade olarak Bussiness Logic'i View'dan kurtarıyor. Peki yeterli geliyor mu bu? Hayır. Çünkü sayfalarımız çoğu zaman UI tarafında da bir tür bussiness taşıyor. Biz bunu Asp.Net MVC'de javascript ile hallediyoruz. Asp.Net'de ise (iğrenç)Postback mimarisi ile hallediyoruz.Tamam sevmiyorum Postback mimarisini ama yine de işimizi oldukça kolaylaştırıyor. Çoğu intranet tabanlı Web uygulaması da -büyük dezavantajlarına rağmen- Asp.Net'i tercih ediyor.
Ben de Asp.Net'i tercih ettim :)
Tabii ki Asp.Net'i MVVM'e uygun hale getirebilmek için biraz üzerinde çalıştım. Mesela Asp.Net sayfasından Model oluşturabilmek için Custom Control ve bu kontrollerden veri toplayan bir Custom DataBinding mekanizması kullandım. Aslında şu anki haliyle çook basit bir durumda bu proje ama hayal ettiklerimi gerçekleyebilmemi sağladı.
Yeterince geyik yaptım :D Artık koda geçelim..
Mimarimizin ana amacı UI Bussiness'ı ViewModel'e taşımak olunca, basit bir UI Bussiness örneği bulmamız gerekiyordu. Ben de en sık karşılaştığımız (bir o kadar da karmaşık :D) Cascading Dropdown örneği üzerinden gerçekledim. Kaskat olarak birbirine bağlı 3 ComboBox'dan İl-İlçe-Semt seçimini gerçekleyeceğiz. Modelimiz sadece bu 3 kontrolün CanSet property'lerini düzenleyerek, View üzerinde işlem yapmaları sağlanacak. O zaman ViewModel'imize bakalım.
Bu arada mimarinin henüz önemli bir kısmı eksik.. O da Model ve ViewModel tek sınıfta. .Ama mantığı taşıması açısından kodun yeterli olduğunu düşünüyorum ;)

public class AddressViewModel : ViewModelBase<AddressViewModel>
{
    //Default olarak ViewModel'in property'leri Get ve Set edilebilir mi (CanGet)
    protected override bool DefaultAccess { get { return true; } }
    
    //Model üzerindeki propertyler
    public int? IlID { get; set; }
    public int? IlceID { get; set; }
    public int? SemtID { get; set; }

    protected AddressService addressDataService = new AddressService();

    //UI Logic ya da UI Bussiness
    protected override void PrepareBussiness()
    {
        this.CreateAccessRule("IlceID", AllowFor.Get | AllowFor.Set, () => this.IlID.HasValue);
        this.CreateAccessRule("SemtID", AllowFor.Get | AllowFor.Set, () => this.IlceID.HasValue);

        this.AddDatasource("IlID", () => this.addressDataService.IlData());
        this.AddDatasource("IlceID", () => this.addressDataService.IlceData(this.IlID.Value));
        this.AddDatasource("SemtID", () => this.addressDataService.SemtData(this.IlID.Value, this.IlceID.Value));
    }
}
Bizim için buradaki önemli kısım "PrepareBussiness" methodu.. Dikkat ederseniz CreateAccessRule ile IlceID, IlID'nin değeri olup olmadığına bağlanıyor. Hakeza SemtID'de IlceID'ye bağlanıyor. Burada dikkat etmemiz gereken bir nokta var. Default ayarda biz Model'imizi erişime açmış olmamıza rağmen, aşağıda Get ve Set property'lerini bir üst değerin var olmasına bağlıyoruz. Yani IlID'nin bir değeri olmadan IlceID Get(Visible) ve Set(Enabled) edilemez.. Meali: İl seçilmeden Ilce görüntülenmeyecek..

Bir diğer önemli nokta da erişimi denetleyen kısımların de birer delegate(Func<bool>) olmaları.. Böylece her erişim denendiğinde; belirtilen Rule çalıştırılacak ve sonuç dönecektir.Tabii ki bu Rule'lar ViewModelBase<> sınıfında Dictionary<string,Func<bool>> içinde tutuluyor.
Peki Model'in property'leri sayfaya nasıl yerleşecekler?
<p>
    İl:
    <bss:MyDropDownList ID="ddlIl" runat="server" BindColumnName="IlID" DataTextField="Title" DataValueField="ID">
    </bss:MyDropDownList>

</p>
<p>
            İlçe:
    <bss:MyDropDownList ID="ddlIlce" runat="server" BindColumnName="IlceID" DataTextField="Title" DataValueField="ID">
    </bss:MyDropDownList>
</p>
 <p>
            Semt:
    <bss:MyDropDownList ID="ddlSemt" runat="server" BindColumnName="SemtID"  DataTextField="Title" DataValueField="ID">
    </bss:MyDropDownList>
</p>

Burada kontrolü sınıfın ilgili kontorlüne Bind edebilmek için kendi Custom Controllerimizi kullanıyoruz. Önemli nokta BindColumnName niteliği.. Bu özellik sayesinde, rekürsif bir metod ile Page ile Model'i bind edeceğiz.
Şimdi.. İşin kalbine gelelim.. Code-Behind'a..
public partial class _Default : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        ViewModelHelper.Run<AddressViewModel>(this);
    }
}
Sizi gülümsetebildiğimi umuyorum :) Hayallerimizi azar azar gerçekliyoruz. Peki ne mi var ViewModelHelper sınıfında ?
public static void Run<TViewModel>(Control mainControl) where TViewModel : ViewModelBase<TViewModel>, new()
{
    TViewModel viewModel = DataBindHelper.GetModel<TViewModel>(mainControl);
    viewModel.RunBussiness();
    DataBindHelper.ConfigureView(mainControl, viewModel);
}
Sistemi ayağa kaldıran kısım burada yatıyor.
  • 3. satır: Rekürsif bir method tüm kontrolleri dolaşıp IMyControl'den türeyen kontrollerin verilerini topluyor ve verilen Model'in BindColumnName'lerinde belirtilen property'lerine basıyor.
  • 4. satır: ViewModel sınfımızda yazdığımız RunBussiness() kısmını aktive ediyoruz.
  • 5. satır: ViewModel sınıfında oluşan rule'lar çerçevesinde, View'ı konfigure eder. (Verilerini Bind Eder, CanGet ve CanSet'ler çerçevesinde Visible ve Enabled'ları ayarlar. Varsa Datasource'larını bind eder.)
3. ve 5. satırdaki methodlar birbirine benzer olduğu, sadece ters çalıştıkları için sadece bir ConfigureView methodunu göstereceğim. Elbetteki kaynak kodlardan tüm diğer ayrıntılara ulaşabilirsiniz.
public static void ConfigureView<TViewModel>(Control mainControl, ViewModelBase<TViewModel> viewModel) where TViewModel : ViewModelBase<TViewModel>, new()
{
    Type dataType = viewModel.GetType();
    var properties = dataType.GetProperties();
    foreach (Control ctrl in mainControl.Controls)
    {
        if (ctrl is IMyControl)
        {
            IMyControl myControl = (ctrl as IMyControl);
            if (string.IsNullOrEmpty(myControl.BindColumnName)) continue;

            PropertyInfo propInfo = properties.
                    Where(pi => pi.Name == myControl.BindColumnName).
                            First();

            object dataValue = propInfo.GetValue(viewModel, null);

            myControl.setValue(dataValue);

            myControl.Visible = viewModel.CanGet(myControl.BindColumnName);
            myControl.ControlEnabled = propInfo.GetSetMethod() != null && viewModel.CanSet(myControl.BindColumnName);

            if (myControl is DataBoundControl && viewModel.HasDataSourceFor(myControl.BindColumnName) && myControl.ControlEnabled)
            {
                (myControl as DataBoundControl).DataSource = viewModel.GetDatasourceFor(myControl.BindColumnName);
                (myControl as DropDownList).AutoPostBack = true;
                (myControl as DataBoundControl).DataBind();
            }
        }
        if (ctrl.Controls.Count > 0)
        {
            ConfigureView(ctrl, viewModel);
        }
    }
}

Buradaki önemli nokta baştan beri söylediğimiz CanGet, CanSet methodlarının burada kullanılmış olması.. ViewModelBase sınıfından Datasource atama ile ilgili diğer methodların da burada işlevsel hale geldiğini görmekteyiz.
Ve son olarak da ViewModelBase sınıfımızdan küçük bir kaç nüans gösterelim :)
public abstract class ViewModelBase<TViewModel> where TViewModel : class,new()
{
..........
   private Dictionary<string, Func<bool>> getSecurityTable { get; set; }
   private Dictionary<string, Func<bool>> setSecurityTable { get; set; }

   internal bool CanGet(string propertyName) { return getSecurityTable.ContainsKey(propertyName) ? getSecurityTable[propertyName]() : DefaultAccess; }
   internal bool CanSet(string propertyName) { return setSecurityTable.ContainsKey(propertyName) ? setSecurityTable[propertyName]() : DefaultAccess; }
..........
   protected void CreateAccessRule(string propertyName, AllowFor allowment, Func<bool> allowMethod)
   {
       if ((allowment & Component.AllowFor.Get) > 0)
       {
           if (getSecurityTable.ContainsKey(propertyName))
           {
               getSecurityTable[propertyName] = allowMethod;
           }
           else
           {
               getSecurityTable.Add(propertyName, allowMethod);
           }
       }
       if ((allowment & Component.AllowFor.Set) > 0)
       {
           if (setSecurityTable.ContainsKey(propertyName))
           {
               setSecurityTable[propertyName] = allowMethod;
           }
           else
           {
               setSecurityTable.Add(propertyName, allowMethod);
           }
       }
   }
..........
}
Burada da AccessRule'ları Dictionary'lere atmamızı ve, CanGet ile CanSet canlı kanlı halleriyle ne kadar küçük birer method olduğunu görmenize yetti sanırım :) Nesneye Dayalı Programlama'ya bu yüzden bayılıyorum. İşlerimizi bu kadar kolay hale getiriyor :D Artık sizi kaynak kodlarla başbaşa bırakıyorum..
Umarım [Yazılım Felsefesi]ne bakışınızı değiştirebilecek, genişletebilecek bir makale olmuştur..
Herkese iyi çalışmalar dilerim..

18 Mart 2011 Cuma

[Yazılım Felsefesi] UI Pattern'ler ve MVVM Üzerine Fikirsel Çalışmalar

Merhaba Arkadaşlar;

[Yazılım Felsefesi] başlıklı makalelerle, sizlerle hayallerimi paylaşacağım.. Sorun olarak alıglayıp; çözüm için zihnimi zorladığım fikirler üzerine fikirler ortaya atacağım..

Uzun zamandır düşündüğüm ama tam olarak çözüm üretemediğim sorun vardı:

Çok katmanlı Mimari'ye uygun olarak tasarlanan Bussiness katmanı -ya da -; sisteme dair Bussiness Logic kodlarını barındırıyor. Ancak UI Logic(ya da Application Logic) olarak ifade edebileceğimiz kodları hiç bir şekilde arayüz bağımsız hale getiremiyorduk.  Daha fazla detaya girmeden hemen kısaca bu kavramları açıklayalım:

  • Bussiness Logic: "Bir e-ticaret portalında satışın onaylanması için girilen Kredi Kartının, sipariş boyutunca provizyon vermiş olması gerekir." gibi sistemsel kural ve mantığa denir.
  • UI Logic(App Logic): "Adres girme ekranında İl combosu seçildiğinde, İlçesi combosu İl'de seçilen değere göre doldurulur." gibi ön arayüzü ya da kısaca Bussiness Katmanından önce bir model'in kullanıcı tarafından manüple edilmesi olaylarına bu ad verilir. "Hesaplar listesinde, 10.000 TL'den büyük aylık geliri olanların kırmızı görünmesi" gibi kural örnekleri de verilebilir.

Asp.Net MVC bu trendin başlancını bence çok güzel verdi. Model üzerine;

  • Validation
  • Control Type(Ön arayüzdeki gösterilecek kontrolün şekli (EditorFor(...) ya da DisplayFor(...) gibi))
  • FormatString
  • DisplayName(Alanın Kullanıcıya gösterilecek ismi)

gibi alanlar yerleştirerek UI'a bu bilgilerin konulmasının önüne geçmeye çalıştı.. Aslında Model üzerine konulabilecek bence önemli bir kısım daha var..

Model Based Security diye sallama bir tabir yaptım bu fikir için :) Amacım aslında Model'lerimize, nesne bazlı ya da property bazlı security ayarları koymak(bu Controller'ların üzerine Authorize(Role="Admin") gibi olabilir). Örnek vermek gerekirse;

  • ForumPost nesnesinin sadece CreateUser tarafından değiştirilebilmesi, 
  • Kisi modelinin Ad, Soyad bilgilerini CreateUser değiştirebilmekteyken, Maaş bilgisini sadece Admin değiştirebilmesi
  • Kisi modelinin tüm kayıtlarını Maaş hariç görebiliyor, kendi maaşını görebiliyor vb.

Her şeyin nesneye dayalı olması için bu da güzel bir teknik ama hala bazı isteklerimizi karşılamıyor. UI Logic için bir çözüm bulmalıyız.. Peki MVVM'e ne dersiniz?

Model-View-ViewModel tabiri bize biraz daha başlancıç oluşturuyor. ViewModel kelimesini de; Model of View'a çevirdiğimizde daha da anlaşılır. İlk aklıma gelen şey, "İl" combosuna bir dropdown gibi erişebilmemizi sağlayacak bir sınıf.. Aslında bu sınıf; ..aspx.cs sınıflarımız oluyor.. Bizim için korkutucu, sadece daha fazla karmaşıklığa neden olan sınıflar.. O zaman biraz daha farklı bir bakış açısından gitmeliyim..

Model'den sayfaya.. Test Driven yazarken; sayfayı hayal edip; kodsal olarak test etmek..

Sayfa üzerinde en sık kullandığımız Applicaiton Logic: Visible ve Enabled.. Peki Model üzerinde nasıl tasarlayabiliriz bunu ?

  • CanGet : Visibility
  • CanSet : Enabled
  • CanInvoke : Button Enabled

olarak ifade etsek model üzerinde ? Ortaya aşağıdaki gibi pseude-code çıksa?

 

model.Ensure(c=> c.Il.HasValue).AllowForSet(c=> c.Ilce);


 



Bu bize aslında testlerimizi de ortada bir UI yokken bile yapabilmemizi sağlamaz mı mesela:



 



public int IlceID{
get
{
if ( ! this.CanGet("IlceID"))
thow new Exception();
return _ilceId;
}
set
{
if ( ! this.CanSet("IlceID"))
thow new Exception();
return _ilceId;
}
}


 



gibi bir kodu rahatlıkla test edebiliriz.



 



public void TestWeCanNotSetIlceBeforeSetIl()
{
AdresModel adres= new AdresModel();
Assert.ThrowsException(()=>adres.IlceId=5);
}

public void TestWeCanSetIlceAfterSetIl()
{
AdresModel adres= new AdresModel();
adres.IlID=3;
Assert.DontThrowException(()=>adres.IlceId=5);
}


 



Tabii ki property'ler sürekli tekrar eden böyle bir koddan Proxy sınıflar aracılığıyla kurtarır ve temiz bir kod elde etmeye çalışırız. Zaten ChangeTracking amacıyla çoğu zaman model'lerimiz proxy nesneler halinde erişmekteyiz - ki bunu da bize ORM'lerimiz sağlar...



Peki bunu nasıl uygulayabilirim. Bir de bunu üzerine beyin jimnastiği yapalım:



 



Asp.Net MVC'de Model'lerin bind edilme mimarisi hazır zaten. Yaptığım ek değişiklikleri de Html.DisplayFor(...) mimarisi ile gerçekleyebilirim. Ancak Controller ile ViewModel'i birbirinden ayırmam çok zor olacak.



Asp.Net WebForms daha stateful olduğu için belki işime yarayabilir. Asp.Net'in kendi kontollerinden inherit alarak kendi Custom Contol'lerimi yazarsam bunları da Model'in property'lerine bind edecek bir sistem geliştirirsem güzel olabilir.



 



Bu fikri sevdim :) Şimdiden heyecanlanmaya başladım :)



Bakalım hayal ettiklerimi ne kadar gerçekleyebileceğimTongue out

16 Mart 2011 Çarşamba

[Tekerleği Yeniden Keşfet] Katılgan ORM

Merhaba Arkadaşlar;

Bu makalenin kaynak kodlarını CodePlex'deki Adresinden (KatılganORM.rar) indirebilirsiniz.

 

ORM(Object Relational Mapper) kelimesini duyunca "Yeni bir ORM mi?" diye sorduğunuzu duyuyor gibiyim Smile Zaten bu yüzden başlığımız [Tekerleği Yeniden Keşfet]..

"Tekerlek bir kez keşfedilmişken, niye yeniden emek vereyim ki?" diye düşünmeyin. İnsanlık 1969'da Amerika'lı astronotların Aya çıkmasının 40 yıl sonrasından bile; Çin uzaya insan göndermeye çalışıyordu. Burada amaç aynı zorlu süreçten geçip; benzer hataları yapıp, ders alıp tecrübe edinmek... Bizde burada kendi küçücük "Katılgan ORM"mizi yaparken aynı şekilde tecrübe etmeye çalışacağız.

Tabii ki çok ihtiyacımız olmadıkça NHibernate'den ya da Entity Framework'den vazgeçmeyeceğiz.. Ama ihtiyacımız olunca da kullanmaktan kaçınmayacağız (Maalesef bir projemde benim ihtiyacım oldu. Laughing)

Amacımız oldukça basit bir ORM çıkartmaya çalışmak. Özellikleri aşağıdaki gibi olacak.

  1. Select All, Select By Example gibi basit işlevleri olacak.
  2. Update,Insert, Delete işlemleri olacak..
  3. Gelen Relational bir kaydı Nesne olarak dönecek(Sanırım Object kelimesi buradan geliyordu :)
  4. Ek olarak Basit bir BaseService aracılığıyla direk DB'ye sorgu atmamıza izin verecek, istersek bir sınıfa map edecek..

Kodumuzu olabilidiğince küçük tutumak için DB bağımsızlığını tam olarak implemente etmeyeceğiz. Bu da size ödev olsun :)

Her şeyden önce DB ve Entity'mizi görelim.

public class Kisi
{
[PrimaryKey(IsIdentity = true)]
public int KisiID { get; set; }
public string Ad { get; set; }
public string Soyad { get; set; }
public string TelNo { get; set; }
}




Gördüğünüz gibi, entity sınıfımız olabildiğince basit.. PrimaryKey Attribute'u var o da yine çok basit bir sınıf.



İlk adım Program.cs üzerinde yeni bir kişi nesnesi oluşturup DB'ye göndermek, ardından da edindiği ID değerini ekrana yazdırmak.



 



DataContext<Kisi> kisiContext = new DataContext<Kisi>();
Kisi yeniKisi = new Kisi { Ad = "Tahir", Soyad = "çakmak", TelNo = "9876543" };
kisiContext.InsertEntity(yeniKisi);

Console.WriteLine("Eklenen Kisi'nin ID'si: {0}", yeniKisi.KisiID);


 



DataContext<T> sınıfımız ORM'imizin tek anlamlı, kod barındıran sınıfı.. Üzerinde bütün işlemlerimiz için birer anlamlı method barındırıyor. Tip Güvenlilik için Generic olarak tasarladık.



 



public class DataContext<TEntity> where TEntity : class, new()
{
........
public TE RunCommand<TE>(Func<DbCommand, TE> dbExecute, string conStr)
{
DbConnection con = this.CreateConnection();
DbCommand cmd = con.CreateCommand();
try
{
con.Open();
return dbExecute(cmd);
}
catch (Exception e)
{
throw new Exception("DB Hatası", e);
}
finally
{
con.Close();
}
}
........
}


 



DataContext sınıfımızın en temel methodu "RunCommand" methodu; bu bize bütün DB isteklerinin tek bir noktadan akmasını sağlıyor. Dikkat ederseniz, bu method bir "Template Method Pattern"inin C# 3.0 implementasyonu.. İçerisine DBCommand'ı parametre olarak alıp, dışarıya veri dönen tüm methodlar için işlem noktası.. Diğer noktalarda, Con'ın açılıp-kapanması, ya da hata'ların yakalanması ile ilgili merkezi nokta..



 



public bool InsertEntity(TEntity entity)
{
return RunCommand(cmd =>
{
object identityValue = GetInsertQry(entity, cmd).ExecuteScalar();
var propInfo = entity.GetType().GetProperties().Where(pInfo => pInfo.GetCustomAttributes(typeof(PrimaryKeyAttribute), false).Length > 0).FirstOrDefault();
if (propInfo != null && (propInfo.GetValue(entity, null) == null || ((int)propInfo.GetValue(entity, null)) == 0))
{
propInfo.SetValue(entity, Convert.ChangeType(identityValue, Nullable.GetUnderlyingType(propInfo.PropertyType) ?? propInfo.PropertyType), null);
}
return true;
});
}

public DbCommand GetInsertQry(TEntity entity, DbCommand cmd)
{
if (entity is BaseEntity) { (entity as BaseEntity).IsSaving = true; }

string columns = "";
string parameters = "";
foreach (PropertyInfo info in typeof(TEntity).GetProperties().
Where(p => p.GetSetMethod() != null
&& p.GetCustomAttributes(typeof(NonDbColumnAttribute), false).Count() <= 0
&& (p.GetCustomAttributes(typeof(PrimaryKeyAttribute), false) as PrimaryKeyAttribute[]).ToList().Where(pr => pr.IsIdentity).Count() <= 0
)
)
{
object columnValue = info.GetValue(entity, null);
if (columnValue != null)
{
columns += string.Format("{0},", info.Name);
parameters += string.Format(" @{0} ,", info.Name);
cmd.Parameters.Add(this.CreateParameter(info.Name, columnValue));
}
}

StringBuilder querySb = new StringBuilder();
querySb.AppendFormat("INSERT INTO {0} ({1}) VALUES ({2})", typeof(TEntity).Name, columns.Remove(columns.LastIndexOf(","), 1), parameters.Remove(parameters.LastIndexOf(","), 1));

cmd.CommandText = querySb.ToString() + "; SELECT SCOPE_IDENTITY();";

return cmd;
}


 



GetInsertQuery methodu adı üsütünde :) InsertQuery'sini Parametreleriyle beraber oluşturup geriye DBCommand döndürüyor.



Burada yaptığı şey aslında çok basit; reflection ile gelen entity'nin tüm değerlerini alıyor, tek tek kolon ismiyle birebir olarak Insert cümleciğine yerleştiriyor; parametre olarak da değerini veriyor. Property'leri dolaşırken önemli bir nokta var. Metodumuz; PrimaryKey ve Identity olan property'leri, NonDbColumnAttribute attribute'una sahip property'leri, ve de Set özelliği olmayan  property'leri DB'ye göndermiyor..  



Peki gelen kayıtları nasıl nesneye dönüştüreceğiz, reflection ve Convert.ChangeType(...) methodu ile Laughing



 



public List<TEntity> GetAllData(string orderByColumn = "", params string[] columnNames)
{
return RunCommand(cmd =>
{
cmd.CommandText = GetSelectQry(columnNames) + (orderByColumn == "" ? "" : " ORDER BY " + orderByColumn);
return MapEntity(cmd.ExecuteReader());
});
}

public string GetSelectQry(params string[] columnNames)
{
StringBuilder sb = new StringBuilder();
sb.Append("Select ");
if (columnNames.Length > 0)
{
for (int i = 0; i < columnNames.Length; i++)
{
sb.Append(columnNames[i]);
if (i != columnNames.Length - 1) { sb.Append(","); }
}
}
else { sb.Append(" * "); }

sb.AppendFormat(" From {0}", typeof(TEntity).Name);
return sb.ToString();
}


 



Yukardıdaki kısımlar, Insert bölümünden farksız.. Yine string işleme.. Ve sistemin kalbi.. Mapping kod bloğu



 



public List<TEntity> MapEntity(IDataReader dr, params string[] exceptColumns)
{
List<TEntity> entityList = new List<TEntity>();
while (dr.Read())
{
TEntity entity = new TEntity();
var props = typeof(TEntity).GetProperties();
foreach (PropertyInfo info in props)
{
foreach (DataRow row in dr.GetSchemaTable().Rows)
{
if (row[0].ToString().ToLowerInvariant() == info.Name.ToLowerInvariant())
{
info.SetValue(entity, FormatHelper.AssignValue(dr[info.Name], Nullable.GetUnderlyingType(info.PropertyType) ?? info.PropertyType, !exceptColumns.Contains(info.Name)), null);
}
}
}
entityList.Add(entity);
}
return entityList;


Burada bize en çok yol gösteren IDataReader üzerindeki GetSchemaTable() methodu. Veritabanından gelen kolonların tiplerini belirtiyor; böylece gerekliyse bizim dönüşümleri sağlamamıza yardımcı oluyor.. Bir diğer durum da Nullable alanların gerçek tiplerini almamızı sağlayan Nullable.GetUnderlyingType metodu..



Kaynak kodları karıştırıyorken, BaseService<T> sınıfıyla karşılaşacaksınız. Bu sınıf DBContext<T> sınfını kullanan; Data Access Facade amacıyla kullanılan Servis katmanı için bir üst sınıfdır.



Sistem genel olarak Select, Update ve Delete için aynı işlemleri tekrar etmek üzerine gidiyor.. Ancak buraya kadar ki kısmın bir sonraki bölümleri incelemkte yeterli olduğunu düşünüyorum. Ve Sizleri kaynak kodlarla başbaşa bırakıyorum Laughing

[Rehber Makale] Distributed Transaction

Merhaba Arkadaşlar;

[Rehber Makale]'ler ile sizlere özel bir konu hakkında bilgi tekrarı olmadan, sizlere pratik tecrübelerimi aktardığım makaleler ulaştırmayı planlıyorum.

Nedir bu Distributed Transaction?

Bir uygulamada oluşan kayıtların birden fazla ve/veya birbirinden farklı markalardaki kayıtların tek bir transaction içerisinde işlem görmesidir. Bizim burada inceleyeceğimiz kısım; WCF üzerindeki uygulamalarıdır.

 

Başlangıç için okunması gereken makaleler:

1. WCF - Transaction Yonetimi (Transaction Management) – 1

2. WCF - Transaction Yonetimi (Transaction Management) – 2

3. 6 steps to enable transactions in WCF

Bu makaleler başlangıç için oldukça iyi fikir verecektir.

Önemli olan sadece bir kaç nokta vardır: WCF Binding Transaction ayarı; 1-2 attribute

Distributed Transaction Coordinator (MSDTC.exe)

Microsoft tarafında distributed transaction görevini üstlenen araçtır. Default olarak Windows sistemlerde kurulu bir servistir... Servislerde yukarıdaki isimlerle görüntüleyebilirsiniz. Com üzerinden iletişime geçilir. (Ki .Net ‘de arka planda Com üzerinden iletişime geçmektedir.) Yapı hakkında net bilgiler veremiyor olsam da şunu söyleyebilirim. WCF üzerinden bir Distributed Transaction(DT) isteğinde bulunduğunuzda(ki TransactionScope bunun için yeterlidir) MSDTC Database dahil tüm transaction işlemlerini kendisi yönetir. Sql Server ve Oracle kendi servisleri aracılığıyla; DTC için API’lar sağlamışlardır. (Oracle için Oracle Client’ın custom kurulumunda görebileceğiniz OracleMTSRecoveryService- MTS(Microsoft Transaction Server)) servisi ile gerçekleşir.

Oracle’ın işi tam olarak nasıl yürüttüğü ile ilgili Oracle® Services for Microsoft Transaction Server Developer's Guide adlı güzel bir kitabı var. Bu kitapta gördüğüm ve DTC’ye ait tek şema aşağıdaki resim bir çok konuda bilgi sahibi etmekte bizleri:

image

 

Sorun çözmek üzerine

MSDTC’nin en kötü yanlarından bir tanesi de sorunun ne olduğunu anlamanın zorluğu... MSDTC’nin çalışmasını gördüğümüz 2 yer var:

1- Component Services(Control Panel’a Administrative Tools) üzerinden

a. Computer - My Computer – Distributed Transaction C.. - Local DTC – Transaction Statistics bölümün de erişiebilirsiniz.

b. Tüm transactionları görebilmek için Local DTC’nin sağ tuş Properties – Trace sekmesinde “Trace All Transactions”ı seçmelisiniz.

c. Statistics ekranındaki verileri sıfırlamak için servisi restart etmeniz gerekli

2- Daha ayrıntılı bilgi için C:\Windows\System32\Msdtc\Trace altında bir log dosyası oluşturuyor.Bu dosyayı okumak için şu makaleyi kullanabilirsiniz. Kısa ayrıntılar aşağıda:

a. Tracemft dosyası için Windows XP Service pack 2 Support Tools’u adresinden indirebilirsiniz. WinRar gibi bir araçla açıp Tracefmt.exe ve traceprt.dll dosyalarını yukarıdaki belirtilen dosya yoluna atın.

b. Eski logları arşivlemek için Component Services’da Local DTC Prop’da Logging Options bölümünde Stop Session – New Session diyebilirsiniz. Eski loglar bir dosyaya atılır.

c. Yaptığınız işleme ait güncel logu dosyadan okuyabilmek için Local DTC’den ‘Flush’ diyerek buffer’dakilerin yazılmasını sağlayabilirsiniz.

d. msdtcvtr –tracelog dtctrace.log –o aa diyerek çalıştırabilir ve logları inceleyebilirsiniz.

3- Sql Server’da çalışıyorsanız; SQL Profiler’da trace etmek için Event’lar arasından (TransactionsàDTC Transactions)’ı seçerseniz DTC transaciton çağrılarını inceleyebilirsiniz.

C# tarafında yazdığınız kodlar üzerinde ise aşağıdaki yöntemleri kullanarak sorunu algılamaya çalışabilirsiniz.

1- Aldığınız hatanın Inner Exception’ında Com hata (hexadecimal)kodunu şuraya sorabilirsiniz.

2- TransactionScope içerisinde o anki transaction’a yakalamak için “Transaction.Current” demeniz yeterlidir.(System.Transactions.dll’i refere etmeniz gerekebilir.)

a. Altındaki TransactionInformation Distributed ID dahil olmak üzere yeterli bilgi vermektedir.

b. Transaction Status’une buradan bakabilirsiniz.

Önemli Not: Debug işlemi uzun sürerse Transaction TimeOut’a düşer.

 

Bilinen Hatalar

Aynı TransactionScope içerisinde hem başka bir WCF servisi çağrılır hemde DB’ye(OracleConnection ile) bağlanılmaya çalışılırsa; connection transaction commit edilmeden kapatılmaması gerekiyor. Aşağıdaki kod örneği bunu açıklıyor:

Bu hatayı temel olarak şu şekilde yakaladım:

1- Transaction.Current.TransactionInformation altındaki Status property’sine Watch ekledim. Kısa bir takip sonucu sorun; ortaya çıktı.

ImyServiceClient myClient = new ImyServiceClient();

using (TransactionScope TranScope = new TransactionScope(TransactionScopeOption.Required))
{
try
{
myClient.AddRecord();
Connection = new OracleConnection(ConnectionString);
Command = new OracleCommand(Query);
Connection.Open();
Command.ExecuteNonQuery();
//Yanlış yerde
//Connection.Close();
TranScope.Complete();
//Doğru yer.
Connection.Close();

}

catch (Exception)
{
TranScope.Dispose();
throw;
}
}


İp Ucu : Servis üzerinden çağrımlarda; Connection.Close() edilme komutu verilmiş olsa dahi; transaction’dan haber bekliyor. Ancak aynı yerde hem yeni bir servis çağrımı hem de direk connection açılması gerekiyorsa yukarıdaki gibi bir sorun ortaya çıkıyor.



İp Ucu: Transaction bittiğinde işlem yapmak istiyorsak; Transaction.Current.TransactionCompleted eventına register olmamız yeterli..