29 Eylül 2007 Cumartesi

WSE 3.0 Security

Web servisinize ulaşan bir mesajın acaba bir saldırımı yoksa gerçekten bir istemci mesajı mı olduğunu nasıl kontrol edersiniz? Peki, mesajın güvenliğini sağladığınızı varsayalım acaba istemci kullanıcının yetkisi dışında bir veriye talepte bulunmadığından emin olabilir misiniz?

WSE 3.0 Policy Framework özelliklerini inceliyoruz.

WSE Policy Framework

Politikalar çalışma zamanı kavramlarıdır. Görevleri istemci veya sunucuyu politikada yer alan bildirgelere göre mesajları zorlamaktır. Bildirgeler mesajları süzen filtrelerdir. Her bildirgede dört adet süzgeç bulunmaktadır. Bu süzgeçler her biri farklı mesaj işleme adımlarında çalıştırılmaktadır.

Geliştirici kendi güvenlik prensiplerine göre bu süzgeçleri oluşturmaktadır. SOAP Filter (süzgeç) nesneleri içinde mesajın bileşenlerine ekleme veya eklenen özellikleri kontrol ederek güvenlik prensiplerimizi gerçekleştirmekteyiz. Bu süzgeçler içerisinde mesajın hepsini şifrelemekte mümkün sertifika bilgilerini değiştirmekte mümkündür. SOAP Filter nesneleri bizim asıl olarak güvenlik kodlarımızı yazacağımız yerlerdir. Hem süzgeç hem bildiri için WSE içinde çok çeşitli kullanmaya hazır üst sınıflar mevcuttur. Biz kendi politikamıza uygun üst sınıfları kullanmalıyız.

Şekilde sistemin tam bir döngüsünü görmekteyiz. Örnek politikamıza iki adet bildirge eklenmiştir. Her bildirgemize ait dört tane SOAP Filter görülmektedir.

İlk olarak istemci bir web servis isteğinde bulunur. Daha sonra bu istek politikamıza ait bildirgelerin SOAP Filter Client Output nesnelerinden geçirilecektir. Bu kısımda mesaja doğrulama (authentication) için ihtiyacımız olacak token ve sunucu tarafında ki süzgeçte kullanılacak diğer güvenlik (security) nesneleri eklenmektedir. Daha sonra mesajımız network ile sunucuya ulaşmaktadır.

WSE sunucuya gelen SOAP mesajlarını doğrulamak (authentication) için Win32’ye ait LogonUser fonksiyonunu çağırmaktadır. LogonUser fonksiyonu varsayılan olarak Windows hesabına ait bir doğrulama (Windows Authentication) denemekte ve mesaja eklemektedir. Eğer doğrulama başarılı olursa oluşturulan Principal nesnesi UsernameToken’nın Principal alanına atanacaktır. Eğer WSE’nin UsernameToken nesnesini Windows ile doğrulamasını istemiyorsanız UsernameTokenManager sınıfın türetmeli ve AuthenticateToken metodunu tekrar yazmalısınız.

Kullanıcı doğrulama adımı geçildikten sonra mesaj ilk olarak politika bildirgelerimizin SOAP Filter Service Input nesnelerine gelmektedir. Eğer mesaj bu adımda ki süzgeçten geçebilirse web servis kodlarımız çalışacaktır. Daha sonra servis geri dönüşü mesajını oluşturacaktır. Oluşturulan geri dönüş (response) mesajı SOAP Filter Servis Output nesnelerinden geçecektir.

En son olarak tekrar istemci tarafa ulaşan geri dönüş mesajı SOAP Filter Client Input süzgeçlerinden geçecek ve web servis çağrımız yerde geri dönüşte bulunarak çağrımız son bulacaktır.

Bir örmek ile devam edelim. Biz mesajımıza Client Output süzgeci içerisinde bir UsernameToken ve aynı token ile mesajı imzasını ekleyeceğiz ve Service Input süzgecinden gelen mesajın önce token ile belirtilen kullanıcı adı ve şifresinin doğru olduğunu daha sonrada mesajın imzasının doğru olduğunu denetleyip mesajın doğruluğundan emin olacağız. Bu güvenlik prensiplerimizi web servisimize ve istemcimize ekleyeceğimiz politika ile uygulayacağız.

Öncelikle istemci tarafında SOAP Client Output süzgeci ile sunucu tarafta doğrulayacağımız token ve mesaj imzasını ekleyelim. Süzgeç için SOAP mesajlarında güvenlik, mesaj imzası ve şifrelemeyi destekleyen SendSecurityFilter sınıfı türetmeliyiz. Süzgecimizi kullanacak bildirge nesnemizi aynı şekilde güvenlik özelliklerine sahip SecurityPolicyAssertion sınıfından türetmeliyiz..

[cs]

Internal class UsernameClientAssertion : SecurityPolicyAssertion {
    private string username;
    private string password;
 
    public UsernameClientAssertion(string username, string password) {
        this.username = username;
        this.password = password;
    }
 
    public override SoapFilter CreateClientOutputFilter(FilterCreationContext context) {
        return new ClientOutputFilter(this, context);
    }
……
class ClientOutputFilter : SendSecurityFilter {
    UsernameClientAssertion parentAssertion;
    FilterCreationContext filterContext;
 
    public ClientOutputFilter(UsernameClientAssertion parentAssertion, FilterCreationContext filterContext)
        : base(parentAssertion.ServiceActor, false, parentAssertion.ClientActor) {
        this.parentAssertion = parentAssertion;
        this.filterContext = filterContext;
    }
 
    public override void SecureMessage(SoapEnvelope envelope, Security security) {
        UsernameToken userToken = new UsernameToken(
            parentAssertion.username,
            parentAssertion.password,
            PasswordOption.SendHashed);
 
        security.Tokens.Add(userToken);
 
        MessageSignature sig = new MessageSignature(userToken);
        security.Elements.Add(sig);
    }
}

Mesajımıza süzgeç içerisinde token ve mesaj imzası eklemiş bulunuyoruz. Artık istemci tarafı güvenlik politikamızı web servis Proxy nesnemize uygulayabiliriz.

[cs]

UsernameClientAssertion assert=new UsernameClientAssertion(userName,password);
Policy policy = new Policy();
policy.Assertions.Add(assert);
serviceProxy.SetPolicy(policy);

İstemci tarafta işimiz bitti şimdi sunucu tarafında istemci tarafta eklenen verileri doğrulayacak ve bu doğrulamaya göre mesajılar süzecek bir politika oluşturalım. Politikalar oluşmadan önce LoginUser metodu çalışacak ve kullanıcı doğrulaması yapılacaktır. Kendi politikamız için kendi kullanıcı doğrulama sınıfımızı yazalım. Senaryoda anlattığımız gibi UsernameTokenManager sınıfını türetmeli ve AuthenticateToken metodunu yeniden yazmalıyız.

[cs]

public class ServiceUsernameTokenManager : UsernameTokenManager {
protected override string AuthenticateToken(UsernameToken token) {
//TODO şifrenin olması gereken halini bul ve return et
    AuthenticationService proxy = new AuthenticationService();
    AuthenticationModel.PERSONELDataTable personel=proxy.AttempSelectUser(id);
     if (personel == null)
        throw new SecurityFault (id ,"Geçersiz kullanıcı ile service erşimi :"+token.Username);
     return personel[0].CH_SIFRE;
}
}

Sunucu politikamızı oluşturalım. İstemci kısmı gibi SOAP Security eklentilerini destekleyen sınıfları türeterek kendi mesaj süzgecimizi ve bildirgemizi oluşturalım.

Şimdi kendi kurallarımızı UsernameServiceAssertion içinde yazabiliriz. Bu politika sunucu tarafında çalışacağı için sadece CreateServiceInputFilter uygulamamız yeterlidir.

[cs]

public class UsernameServiceAssertion : SecurityPolicyAssertion {
    public override SoapFilter CreateServiceInputFilter(FilterCreationContext context) {
        return new ServiceInputFilter(this, context);
    }
public override void ReadXml(XmlReader reader, IDictionary<string, Type> extensions) {
    // TODO: bildirgenin kendisine ait node’u bul oku.
    // mesaj işlemeye devam etsin
    reader.ReadStartElement(tagName);
}

Süzgeç içinde gelen mesajın doğrulamasını yapacağız:

[cs]

public class ServiceInputFilter : ReceiveSecurityFilter {
public override void ValidateMessageSecurity(SoapEnvelope envelope, Security security) {
    bool IsSigned = false;
    if (security != null) {
        foreach (ISecurityElement element in security.Elements) {
            if (element is MessageSignature) {                            
                MessageSignature sign = element as MessageSignature;
                //Beklenen ve gelen mesaj özelliklerini karşılaştır
                if (CheckSignature(envelope, security, sign)) {                            
                    if (sign.SigningToken is UsernameToken) {                            
                        IsSigned = true;
                        // mesaj için gecerli token nesnesi olarak imzanın token nesnesini ata
                        envelope.Context.IdentityToken = sign.SigningToken;
                    }
                }
            }
        }
    }
    if (!IsSigned)
        throw new SecurityFault ("Mesaj güvenlik sorgulaması başarısız oldu. Gecersiz mesaj imzası.");
}………………

Sunucu politika nesnemizi kodlama ile veya config üzerine xml ile oluşturabiliriz.

[cs]

public class ServerPolicy : Policy{
    public ServerPolicy() {
        this.Assertions.Add(new UsernameServiceAssertion());
    }
}

[xml]

<policies xmlns="http://schemas.microsoft.com/wse/2005/06/policy">
    <extensions>
        <extension name="usernameAssertion" 
type="UsernameAssertionLibrary.UsernameServiceAssertion, 
 UsernameAssertionLibrary"
  />
    </extensions>
    <policy name="ServerPolicy">
        <usernameAssertion />
    </policy>
</policies>

  Her iki şeklinde etkisi aynı olacaktır. İçerisinde sadece UsernameServiceAssertion bildirgesi olan bir politika oluşturacaktır. Daha sonra bu politikayı kendi web servisimize uygulamamız gerekmektedir.

[cs]

[Policy("ServerPolicy")]
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class Service : System.Web.Services.WebService {

Artık Service web servisimiz ServerPolicy ile belirlediğimiz kurallara uyan mesajları alacaktır. Önce istemci tarafından kullanıcı adı ve şifre ile bir token oluşturduk bu token ile mesajı imzaladık. Daha sonra sunucu tarafında bu token ile kullanıcı (şifre) doğrulaması yaptık. Daha sonra mesaj imzasını ve mesaj özelliklerini test ettik. Tüm bu adımları gecen mesajın politikamıza uyan güvenli bir mesajdır.

Mesajın güvenliğinden emin olduğumuza göre son bir işlemimiz kaldı. İstekte bulunan kullanıcıya ait diğer bilgileri bir SOAP başlığına ekleyecek ve kullanıcının yetkisi dışında bir istekte bulunup bulunmadığını kontrol edeceğiz.

[cs]

public CustomSoapHeader header;
 
[WebMethod,SoapHeader("header")]
public byte[] GetAllRaporRow() {
    if(header.BolgeKod != 1000 )
         throw new SecurityFault ("Yetkiniz olmayan bir veriye 
 erişmeye çalışıyorsunuz"
 );
    //TODO birşeyler yap
    return new byte[]{};
}
………
public class CustomSoapHeader : SoapHeader {
    private int _bolgeKod;
 
    public int BolgeKod {
        get {
            return _bolgeKod;
        }
        set {
            _bolgeKod = value;
        }
    }

İstemci tarafında başlık verisini oluşturalım ve mesaja ekleyelim.

[cs]

serviceProxy.CustomSoapHeaderValue = new CustomSoapHeader();
serviceProxy.CustomSoapHeaderValue.BolgeKod = 1000;

8 Ağustos 2007 Çarşamba

Rapor Kütüphanesi Uygulaması

Birçok ipucu içeren ve her zaman başımıza gelebilecek bir uygulama örneğini anlatacağım. Senaryomuz şu:

Minimum 5K-50K arasında kayıta sahip ve veri tabanı maliyeti değerleri çok fazla olan raporlarımız var. Raporlar sunucu tarafında oluşturuluyor ve web servisler ile istemci uygulamalara dağıtılıyor. Bu veriyi Xml formatında istemciye göndermeye çalıştığınızda en küçük veri boyutu 10KB civarında olmaktadır. Xml web servisleri için önerilen veri boyutu maksimum 2KB’dır. O halde yapmamız gerekenler:

  1. İstemci kendisine gelen raporu disk üzerinde saklasın aynı rapor talep edildiğinde bir daha network ve sunucu sistemi meşgul etmesin
  2. Veri boyutunu mümkün olduğu kadar düşürerek network kaynaklarını yormayalım,
  3. İstemci gibi sunucuda oluşturduğu raporu saklasın ve bir raporu veri tabanından bir defa çeksin
  4. Veri güvenliğinde sağlayalım

Çözüm gereği kullanıcı bir rapor talep ettiğinde önce yerel diske bakılacak eğer uygun rapor yoksa uzak sistemden talep edilecektir. Uzak sistemde kendi diskinde rapora arayacak eğer yoksa veri tabanından hesaplatacaktır. Raporu elde eden sistem daha sonra ki çağrılar için raporu disk üzerinde saklayacaktır.

DataSerializer

Veriyi xml formatında serialize etmeye çalıştığımız zaman veri boyutumuz xml taglarından dolayı çok fazla artacaktır. Veri boyutunun düşürmek için varsayılan System.Data.DataSet serilalize formatı olan XMLSerializer’dan vazgeçmeli ve kendi DataSerializer sınıflarımızı yazmalıyız. Bu sınıfın görevi veriyi byte[] olarak serialize edip tekrar byte[]’den veri tipimize geri çevirmek.

İpucu 1: DataTable Binary Serialize

[cs]

public byte[] Serialize(params DataTable[] table) {
 lock (_lockObj) {
  BinaryFormatter formatter = new BinaryFormatter();
  MemoryStream stream = new MemoryStream();
  BinaryDataSet binSet = new BinaryDataSet(table);
  formatter.Serialize(stream, binSet);
  return stream.ToArray();
 }
}

Burada aslında tüm işi BinaryDataSet sınıfının yaptığı gayet acık. BinaryDataSet sınıfı çok sade bir mantıkla kendisine verilen DataTable[] dizisine ait tüm tabloların kolonlarını ve kayıtlarını serialize etmektedir.

[cs]

[SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)]
void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context) {
 info.AddValue("DataSetName", _dataSet.DataSetName);
 info.AddValue("TableCount", _dataSet.Tables.Count.ToString());
 for (int i = 0; i < _dataSet.Tables.Count; i++) {
  List colNames = new List();
  List colTypes = new List();
  List dataRows = new List();   info.AddValue(i.ToString()+"TableName",_dataSet.Tables[i].TableName);   // kolon bilgilerini ekle   foreach (DataColumn col in _dataSet.Tables[i].Columns) {    colNames.Add(col.ColumnName);    colTypes.Add(col.DataType.FullName);   }   // row bilgilerini ekle   foreach (DataRow row in _dataSet.Tables[i].Rows)    dataRows.Add(row.ItemArray);   info.AddValue(i.ToString() + "ColNames", colNames);   info.AddValue(i.ToString() + "ColTypes", colTypes);   info.AddValue(i.ToString() + "DataRows", dataRows);  } }

İpucu 2:DataTable Binary Deserialize

[cs]

public DataSet Deserialize(byte[] data) {
 Guard.ArgumentNotNull(data, "data");
 if (_lastData == data)
  return _lastDataSet;
 lock (_lockObj) {
  BinaryFormatter formatter = new BinaryFormatter();
  MemoryStream stream = new MemoryStream();
  stream = new MemoryStream(data);
  formatter.Binder = new DeserializationBinder();
  BinaryDataSet binDataSet = 
   (BinaryDataSet)formatter.Deserialize(stream);
  if (binDataSet != null) {
   return binDataSet.DataSet;
  }
  return null;
 }
}

public TableType Deserialize(byte[] data) where TableType : DataTable { 
 DataSet dataSet = Deserialize(data);
 TableType result = Activator.CreateInstance();
 if (dataSet != null && dataSet.Tables.Contains(result.TableName)) {
  result.BeginLoadData();
  foreach (DataRow row in dataSet.Tables[result.TableName].Rows) {
   result.ImportRow(row);
  }
  result.EndLoadData();
 } else {
  throw new ApplicationException("Tablo:" + result.TableName 
   + " veri kümesi içinde mevcut değil.");
 }
 return result;
}

İlk Deserialize fonksiyonu verilen byte[] ile bir DataSet nesnesi oluşturmaktadır. İkinci Deserialize fonksiyonu aslında daha çok kullandığımız versiyonudur. İkinci versiyonda byte[] önce DataSet nesne çevirir daha sonra istenen DataTable nesne tipi ile uyuşan tablo aranır ve eğer bulunursa sadece bu tablo geri döndürülür

İpucu 3: Serializetion header bilgisini değiştirmek

Serializtion verisinin ilk 1024byte’lık ilk bölümünü seriaselize edilen tipe ait Assembly bilgileri yazılmaktadır. Bu bilgiler daha sonra deserialize işleminde Activator.CreateInstance(Type) ile nesneyi tekrar oluşturmak için kullanılmaktadır. DataSerializer sınıfının içinde bulunduğu dll hem sunucu hem istemci tarafında kullanılacaktır. Fakat sunucu tarafında byte[] çevirdiğiniz DataSet nesnesi içinde sunucu tarafında ki dll’e ait veriler bulunacağı için bu şekilde istemci tarafında açmaya çalıştığınızda bize Assembly bulunamadı hatası üretecektir. Bunun için byte[] serialize bilgisi içinde ki Assembly bilgisini tekrar göstermek gerekmektedir.

[cs]

internal sealed class DeserializationBinder : SerializationBinder {
 public override Type BindToType(string assemblyName, string typeName) {
  Type type;
  if (typeName.IndexOf("BinaryDataSet") > -1)
   type = typeof(BinaryDataSet);
  else if (typeName.IndexOf("BinaryDataTable") > -1)
   type = typeof(BinaryDataTable);
  else
   type = Type.GetType(String.Format("{0}, {1}",
     typeName, assemblyName));
  return type;
 }
}

Yukarıdaki yöntem bir nesnenin farklı sürümlerini açmak içinde kullanılabilinir.

Artık DataTable nesnelerimizi byte[] çevirebilmekteyiz ve aynı byte[] bilgisini farklı Assembly’ler içerisinde kullanabilmekteyiz. Ayrıca veri byte olarak sakladığımız için veri okunabilir halde değildir.

Veriyi hazırladığımıza göre rapor kütüphane servisini yazabiliriz.

Report Library Service

Rapor kütüphane servisi DataTable nesnelerini byte[] olarak disk üzerine kayıt eden ve bu kayıtları bir index dosyası ile takip eden bir servistir. Verinin DataTable ile byte[] arasında ki dönüşümleri bir önce ki kısımda tartışılan DataSerializer sınıfı ile yapmaktadır.

Servis her raporu index üzerinde saklayabilmek için bir kriter nesnesine ihtiyaç duymaktadır. BaseKriter sınıfından türetilen kriter nesneleri ile raporlar tipleri oluşturulmakta kriter nesnesine ait diğer property’ler ile rapor kriter verisi oluşmaktadır. Böylelikle raporu oluşturan kriter nesnesinin özellikleri ile unique bir HashCode oluşturulmaktadır.

İpucu 4: Raporlar için kriter özelliklerinden HashCode oluşturma

[cs]

Public string GetHashCode(object obj) {
 return Tip2String(obj) + "[" + Kriter2String(obj) + "]";
} 

private string Kriter2String(object obj) {
 Type type = obj.GetType();
 string result = string.Empty;//type.Name;
 foreach (PropertyInfo info in type.GetProperties()) {
  if (info.Name != VERSIYON_PROPERTY 
   && info.Name != RAPORAD_PROPERTY) {
   result += "[" + info.Name + ":";
   if (info.PropertyType.FullName.IndexOf("System") > -1) {
    if (info.GetValue(obj, null) != null)
     if (info.PropertyType == typeof(DateTime))
      result += ((DateTime)(info.GetValue(obj, null)))
       .ToString("yyyyMMdd");
     else
      result += info.GetValue(obj, null).ToString();
    else
     result += string.Empty;
    result += "]";
   } else if (info.GetValue(obj, null) != null) {
    result += Kriter2String(info.GetValue(obj, null));
   }
  }
 }
 return result.ToString();
}
private string Tip2String(object kriter) {
 Type type = kriter.GetType();
 PropertyInfo info = null;
 info = type.GetProperty(VERSIYON_PROPERTY);
 Guard.ArgumentNotNull(info, VERSIYON_PROPERTY);
 int versiyon = (int)info.GetValue(kriter, null);
 info = type.GetProperty(RAPORAD_PROPERTY);
 Guard.ArgumentNotNull(info, RAPORAD_PROPERTY);
 string raporAd = (string)info.GetValue(kriter, null);
 if (string.IsNullOrEmpty(raporAd))
  raporAd = type.Name.Replace("Kriter", "");
 return raporAd + "_v" + versiyon.ToString();
}

Rapor kütüphane servisi Raporları oluştururken önce raporun verisini byte[] çevrilir daha sonra raporun kriter nesnesinden rapor tipi ve rapor kriteri bilgilerini alırın index üzerine yeni rapor kayıttı eklenir ve son olarak byte[] çevrilmiş rapor verisi ayrı bir dosya olarak saklanır.

[cs]

Public byte[] CreateReportData(object objKriter, params DataTable[] tables) {
 Guard.ArgumentNotNull(tables, "tables"); 
 byte[] data = DataSerializer.Serialize(tables);
 CreateReportData(objKriter, data);
 return data;
}

public void CreateReportData(object objKriter, byte[] data) {
 Guard.ArgumentNotNull(data, "data"); 
 string tip = Tip2String(objKriter);
 string kriter = Kriter2String(objKriter);
 RaporModel.RAPORRow newRapor = null;
 ……
 newRapor = CreateReportRow(tip, kriter);
 ………
 _raporTable.AcceptChanges();
 File.WriteAllBytes(RaporPath + newRapor.SQ_RAPOR_NO + MRP_EXTANSION, data);
}

İpucu 5: Tersine hesaplama ile string veriden nesne oluşturma

[cs]

Public object String2Tip(string tipString) {
 PropertyInfo infoVersiyon = null;
 PropertyInfo infoRaporAd = null; 
 foreach (Type type in Assembly.GetCallingAssembly().GetTypes()) {
  infoVersiyon = type.GetProperty(VERSIYON_PROPERTY);
  infoRaporAd = type.GetProperty(RAPORAD_PROPERTY);
  if (infoVersiyon != null && infoRaporAd != null) {
   object kriter = Activator.CreateInstance(type); 
   if (Tip2String(kriter) == tipString)
    return kriter;
  }
 }
 return null;
}

public object String2Kriter(string kriterString, Type type) {
 object t = Activator.CreateInstance(type);
 string prop, val; int pos, len; object propVal = null;
 foreach (PropertyInfo info in type.GetProperties()) {
  if (info.Name != VERSIYON_PROPERTY 
   && info.Name != RAPORAD_PROPERTY) {
   if (info.PropertyType.FullName.IndexOf("System") > -1) {
    if (kriterString.Contains(info.Name)) {
     prop = "[" + info.Name + ":";
     pos = kriterString.IndexOf(prop) + prop.Length;
     len = kriterString.Substring(pos).IndexOf("]");
     val = kriterString.Substring(pos, len);
     if (!string.IsNullOrEmpty(val)) {
      if (info.PropertyType == typeof(string)) {
       propVal = val;
      }else if (info.PropertyType.FullName.IndexOf("Decimal") > -1){
       propVal = Convert.ToDecimal(val);
      }else if (info.PropertyType.FullName.IndexOf("DateTime") >-1){
       propVal = new DateTime(Convert.ToInt32(val.Substring(0,……
      }
      info.SetValue(t, propVal, null);
     } 
    }
   } 
  } else if (info.Name == RAPORAD_PROPERTY) {
   info.SetValue(t, type.Name.Replace("Kriter", ""), null); 
  }
 }
 return t;
}
 

Bol miktarda reflaction ve generic kullanarak rapor kütüphanesinin zor kısmını halletmiş bulunmaktayız. Kullandığımız mantık veri tabanında raporları oluşturmak için verilen parametreleri saklayan kriter entity nesnelerimizi anahtar olarak kullanıp oluşan raporları disk üzerinden saklamak ve takip etmektir. Aynı kriter değerleri ile bir kez daha rapor istendiğinden disk üzerinde hazır bulunan veriyi kullanılmaktadır.


[cs]

public byte[] GetReportDataById(int reportId) {
 RaporModel.RAPORRow rapor = _raporTable.FindBySQ_RAPOR_NOFL_SERVER(
  reportId, (System.Web.HttpContext.Current != null));
 if (rapor != null) {
  if (File.Exists(RaporPath + rapor.SQ_RAPOR_NO.ToString() + MRP_EXTANSION)) {
   AddRating(rapor);
   return File.ReadAllBytes(RaporPath 
    + rapor.SQ_RAPOR_NO.ToString() + MRP_EXTANSION);
  } else {
   rapor.Delete();
   SaveIndexFile();
  }
 }
 return null;
}
 

RAPORDataTable içerisinde yer alan FL_SERVER alanı çalışma alanı sunucumu istemcimi olduğunu gösteren bir bayraktır. System.Web.HttpContext.Current nesnesi IIS üzerinden gelen çağrılarda null’dan farklı bir değer almaktadır. Böylelikle rapor kütüphanesi sunucu ve istemci için her iki tarafta da oluşturulabilir ve kullanılabilir. Kütüphane içerinde aynı tipe ait raporlar JoinReports ile birleştirilebilmektedir. Ayrıca index verisinin treelist şeklinde gösterimine uygun olarak RAPOR_VIEWDataTable sınıfı ve bu sınıfı destekleyen GetReportView fonksiyonları mevcuttur. Böylelikle rapor kütüphanemiz profesyonel uygulamaları destekleyebilecek donanıma sahiptir olmaktadır.

Geri kalan kısımlar için küçük bir bakış yapalım:

[cs]

/// 
/// Rapor Kütüphanesi arayüzü
/// 
/// 
/// Fonksiyon postfix anlamları:
/// Row: Sadece rapor.indx dosyasına ait rapor kütüphanesi index verisi içinde işlem yapar 
/// Data: disk üzerinde yazılan byte[] verileri ile çalışır 
/// View: arayüz işlemlerini destekleyen raporview tablosu ile çalışır
/// Fonksiyon prefix anlamları:
/// CreateReport: Yeni bir rapor oluştur
/// GetAllRapor: Tüm raporlar bilgilerini getirir
/// GetRapor: Verilen parametrelere göre rapor(ları) getirir
/// 
Public interface IReportLibraryService

Cach Extansion

Son olarak web servisler tarafından kullanılacak rapor kütüphanesi için bir eklenti daha yazacağız. CachExtansion sınıfı talep edilen raporu Ram ve disk üzerine yazmaktadır. Yeni bir rapor talebi geldiğinde raporu önce Ram üzerinde yoksa disk üzerinde aramaktadır.

İpucu 6: Web Side Caching

[cs]

public static byte[] SetToCache(HttpContext context, 
  object key,params DataTable[] table) {
 byte[] data = ReportLibrary.CreateReportData(key, table);
 string strKey = ReportLibrary.GetHashCode(key);
 context.Cache.Insert(strKey, data, null, 
  DefaultExpration, DefaultSliding);
 return data;
}

public static byte[] GetFromCache(HttpContext context, object key) {
 string strKey = ReportLibrary.GetHashCode(key);
 // ram üzerinde ki cachde var mı?
 byte[] data = (byte[])context.Cache[strKey];
 if (data == null) {
  // ram üzerinde yok disk üzerinde var mı ? 
  data = ReportLibrary.GetReportDataByKriter(key);
  if (data != null) {
   // dosyayı 5 dk erişim olmasa cachden sil
   context.Cache.Insert(strKey, data, null, 
    DefaultExpration, DefaultSliding);
  }
 }
 return data;
}
 

Birde web servis içinde CachExtansion sınıfının nasıl kullanıldığına bakalım.

[cs]

[WebMethod] 
public byte[] GetBolgeSatisRapor(BolgeSatisKriter kriter) { 
 byte[] result = CachExtansion.GetFromCache(this.Context, kriter);
 if (result == null) {
  HedefRaporModel.CIRO_RAPORDataTable table = 
   _adapterCiro.GetData(kriter.BaslangicTarih, kriter.BitisTarih); 
  result = CachExtansion.SetToCache(this.Context, kriter, table);
 }
 return result;
}
 

Rapor kütüphanesi raporlama uygulamaları için çok değerli bir bileşendir. Benim çözümüm veri tabanı maliyetinin yüksek olan raporlar için raporu bir defa üretmek ve üretilen raporu saklamak paylaşmak şeklindedir. Her sistem talep edilen raporu kendi local diski üzerinde arayacak bulamaz ise uzak sistemden isteyecektir.




29 Temmuz 2007 Pazar

Injection Pattern

Uygulamanız içinde nesneler arasında ilişkileri nasıl kontrol edersiniz? Mesela uygulamanızda oluşturulan her DbConnection nesnesini bir havuz üzerinde toplamanız gerekti ve bu ihtiyaç projenin ortasında oluştu ne yaparsınız? Geriye dönüp tüm kodları yenileyecek misiniz? Ya da bir sınıfın her örneğinin config dosyanızda yazılan parametrelere göre oluşturulmasını istiyorsunuz ne yaparsınız?

Aşağıda örnek bir Stok projesinin parçaları ve bu parçaların bir birleri ile olan ilişkisi mevcuttur.

Şimdi klasik bir yöntemle bu projeyi kodlayalım. Muhtemel kodlamamız şu şekilde olacaktır.

[cs]

public class App {
    public App() {
        quotes = new StockQuotes();
        authenticator = new Authenticator();
        database = new Database();
        logger = new Logger();
        errorHandler = new ErrorHandler();
    }
}


Uygulama sınıfımız için alt parçalara ulaşmak gayet acık ya alt parçalar birbirleri ile nasıl haberleşir. Yani StockQuotes nasıl Logger elemanını bulur? Veya Database kullanıcı yetkilendirmesini nasıl yapar?

Muhtemel kodlamanızda her bir parça kendi içinde ihtiyaç duyduğu diğer sınıfları yeniden oluşturacaktır.

[cs]

public class StockQuotes {
    public StockQuotes() {
        logger = new Logger();
        errorHandler = new ErrorHandler();
    }
    internal Logger logger;
    internal ErrorHandler errorHandler;
}


Burada bir sorun görüyor musunuz? Aynı sınıfa ait nesneler mükerrer defa oluşturulmakta. Bu durumu iyileştirmek için tüm nesneleri içinde saklayacak yeni bir sınıf yazalım. Tüm gerekli nesneleri bu sınıf içinde bir defa oluşturalım ve daha sonra aynı sınıf üzerinden erişelimi

[cs]

public interface ILocator {
    TObject Get();
    object Get(Type objectType);
}
 
public class MyLocator :ILocator {
    protected Dictionary dict;
 
    public MyLocator() {
        dict = new Dictionary();
 
        dict.Add(typeof(ILogger), new Logger());
        dict.Add(typeof(IErrorHandler), new ErrorHandler(this));
        dict.Add(typeof(IStockQuotes), new StockQuotes(this));
        dict.Add(typeof(IDatabase), new Database(this));
        dict.Add(typeof(IAuthenticator), new Authenticator(this));
        dict.Add(typeof(App), new App(this));
    }
}


Tüm sınıfları saklayan anahtar olarakta sakladığı sınıfın arayüzünü kullanan bir üst sınıf ile mükerrer defa nesne oluşturma problemini aşabiliriz. Uygulama kodlarımız şu şekilde olacaktır:

[cs]

public App(ILocator locator) {
    quotes = locator.Get();
    authenticator = locator.Get();
    database = locator.Get();
    logger = locator.Get();
    errorHandler = locator.Get();
}
 
public class StockQuotes :IStockQuotes{
    public StockQuotes(ILocator locator) {
        logger = locator.Get();
        errorHandler = locator.Get();
    }…


Bu çözümün güzel tarafı sınıflar ihtiyaç duydukları sınıfları kolaylıkla alabiliyorlar.

Ama bizim için tam bir çözüm değil. Her obje ILocator ile ilişkili, ayrıca nesne oluşturma sırası hala problemli. Uygulama sırasında anlık olarak kullanılmayan nesneler hala saklanmaktadır. O halde birde LifeTime Continer nesnesine ihtiyacımız var. Locator nesneleri bu LifeTimeContainer üzerinde saklamalı LifeTimeContainer referansı kalmayan nesneleri kendisi temizlemelidir.

LifeTime Continer nesnesini kullanabilmemiz için tüm oluşan objelerden haberdar olmamız gerekmektedir. Madem her nesne oluşturulurken çalışacak bir sınıf yazacağız bu sınıfı Stragy Pattern destekler şekilde yazalım daha sonradan eklenecek yeni Stragy sınıfları ile entegre edebilelim. Tabi çok fazla sayıda Stragy ve Policy sınıfı olunca bunların bir sorumluluk zinciri şeklinde çalışmaları gerekmektedir. P&P grubunun hazırladığı Builder sınıfı bizim için bu işlemi yapmaktadır.

Nesnelerin Creation anını kontrol edebildiğimize göre artık nesnelere ihtiyaç duydukları tüm nesneleri ‘enjekte’ edebiliriz. Önce Builder sınıfını uygulayacak bizim örnek uygulamamızda tüm nesneleri yönetecek örnek bir DependencyContainer sınıfı yazalım ve daha sonra nesnelere enjekte olayının nasıl uygulandığına bakalım.

[cs]

public DependencyContainer() {
    _builder = CreateBuilder(null);
    _locator = new Locator();
    _container = new LifetimeContainer();
    _locator.Add(typeof(ILifetimeContainer), _container);
}
 
private IBuilder CreateBuilder(IBuilderConfigurator  configurator) 
{
 IBuilder result = new BuilderBase();
 result.Strategies.AddNew(BuilderStage.PreCreation);
 result.Strategies.AddNew(BuilderStage.PreCreation);
 result.Strategies.AddNew(BuilderStage.PreCreation);
 result.Strategies.AddNew(BuilderStage.PreCreation);
 result.Strategies.AddNew(BuilderStage.PreCreation);
 result.Strategies.AddNew(BuilderStage.Creation);
 result.Strategies.AddNew(BuilderStage.Initialization);
 result.Strategies.AddNew(BuilderStage.Initialization);
 result.Strategies.AddNew(BuilderStage.PostInitialization);
 result.Policies.SetDefault(new DefaultCreationPolicy());
 if(configurator != null)
      configurator.ApplyConfiguration(result);
 return result;
}


Kendi özel Strategy sınıflarımızı yazıp burada DependencyContainer üzerine ekleyebiliriz. Bu tüm nesne Creation işlemlerinde çalışacak bir kod bloğu manasına gelir. Uygulamaya sonradan kazandırılacak yetenekler için harika bir durumdur.

DependencyContainer sınıfımıza diğer nesnelere hizmet verecek şekilde biraz daha genişletelim.

[cs]

public TBuild Get() {
    return _builder.BuildUp(_locator, null, null);
}
 
public void RegisterSingleton() {
    _builder.Policies.Set(
 new SingletonPolicy(true), typeof(TBuild), null);
}
 
public void RegisterTypeMapping() {
    _builder.Policies.Set(
 new TypeMappingPolicy(typeof(TToBuild), null), typeof(TRequested), null);
}


Artık nesneler DependencyContainer içinde register olan her hangibir sınıf örneğini kolayca Get<T>() ile alabilirler.Nesneler oluşurken kendi ihtiyaçlarını Builder sınıfı sağlayacaktır.

[cs]

DependencyContainer container = new DependencyContainer();
IStockQuotes stock = container.Get();
 
public class StockQuotes :IStockQuotes {
    public StockQuotes(IErrorHandler handler, Logger logger) {
        _errorHandler = handler;
        _logger = logger;
    }
 
    Logger _logger;        
    public Logger Logger {
        get {
            return _logger;
        }
    }
 
    IErrorHandler _errorHandler;
    public IErrorHandler ErrorHandler {
        get {
            return _errorHandler;
        }
    }
}


Sadece constructor içinde değil property veya fonksiyonlara da enjekte yapabiliriz.

[cs]

public class Database :IDatabase {
    public Database() {            
    }
    Logger _logger;
    [Dependency]
    public Logger logger {
        set {
            _logger = value;
        }
        get {
            _logger;
        }
    }
    [InjectionMethod]
    public void Init(IErrorHandler handler) {
        
    } 
}

DependencyContainer veya CAB içinde ki adı ile Builder sınıfı konfigürasyon ile nesne oluşturabilir. Bunun için hazırlanmış IBuilderConfigurator ara yüzü vardır. Kullanımı insan sihirli kod bloğu hissini verir. Nesneleriniz sizin config dosyası üzerinde yazdığınız verilere göre oluşmaktadır. Tüm property metot parametreleri her şeyi config üzerinden halledebilirsiniz.

<ContainerConfig xmlns='container-config'>
    <Mappings>
        <Mapping FromType='Object1' ToType='Object1'/>
    </Mappings>
    <BuildRules>
        <BuildRule Type='Object1' Mode='Instance'>
            <Constructor>
                <Value Type='System.String'>ABC</Value>
            </Constructor>
            <Method Name='SetLength'>
                <Value Type='System.Int32'>123</Value>
            </Method>
        </BuildRule>
    </BuildRules>
</ContainerConfig>

[cs]

public class Object1 {
    public Object1(string color) {
        _color = color;
    }
    public void SetLength(int length) {
        _length = length;
    }

Şimdi burada durup birde büyük resme bakalım. ObjectBuilder kütüphanesi tüm P&P grubu ürünlerinde başrol oynamaktadır. Biz farkında olmasak bile mesela bir xml üzerinden config verilerini okurken bile bu kütüphane iş başındadır. CAB içerisinde her WorkItem nesnesi kendi Builder nesnesine sahiptir. Her WorkItem RootWorkItem ile entegre çalıştığı için asıl LifeTime Container RootWorkItem üzerindedir.

Böylelikle en zor konu olan Injection Pattern sonuna geldik. Eğer bu konuyu anladı iseniz P&P grubuna ait tüm ürünleri rahatlıkla kullanabilirsiniz. P&P grubu bu pattern ile nasıl harika işler çıkartmış incelemeye devam edeceğiz. Hafta gerçek programlar yazmaya SCSF ile uygulama geliştirmeye başlıyoruz. 

17 Mayıs 2007 Perşembe

MS Message Queue ile Asenkron Programlama

Birden çok programı asenkron konuşturmak gerekirse ne yaparsınız?  Bir sunucu üzerinde veriyi çekeceksiniz başka bir sunucu ile o veriye ait hesaplamaları yapacaksınız üçüncü bir sunucu ile yeni iş süreçleri oluşturup farklı sistemleri ve kişileri haberdar edeceksiniz. Tüm bu sistem tamamen birbirinden yalıtılmış ve tamamen asenkron olması gerekiyorsa ne yapmalısınız?

MSMQ en basit çalışma yapısı şekilde gösterildiği gibidir. Birçok iş uygulamasında uzak sistemler arasında sender-reciver mantığı ile çalışan haberleşme mekanizmalarına sahiptir. Dağınık programlar arasında ki veri iletişimi MS Message Queue (MQ) ile yapılabilmektedir. MQ programların birbirleri ile asenkron haberleşmek için kullanabilecekleri bir FIFO kuyruktur. ObjectFormater nesnesini desteklediği için object remoting içinde kullanılabilir. Transaction desteğinden server cluster desteğine kadar iş ihtiyaçlarını karşılayabilecek güçlü bir yapıya sahiptir.
MQ işlemleri System.Messaging isim uzayı kullanılır. Kodlama ile bir kuyruk açabilir, kuyruğa yeni mesajlar ekleyebilir ve sıra ile mesajları okuyabilirsiniz.
MessageQueue nesnesine üzerinden çalışılacak message path ve formater'ı vermelisiniz.

[cs]
// eğer messageQ yoksa oluştur
if (!MessageQueue.Exists(this.SMSQInfo.QPath)) {
 MessageQueue.Create(this.SMSQInfo.QPath, true);
}
smsQ = new MessageQueue();
smsQ.Path = this.SMSQInfo.QPath;
((XmlMessageFormatter)smsQ.Formatter).TargetTypeNames = 
 new string[] { "System.String,mscorlib" };
Artık oluşturduğumuz MQ üzerine yeni mesajlar ekleyebiliriz. Send edilen bir mesajın reciver kuyruk tarafından alınması için reciver olarak kullanılan makinada MQ servisinin açık olması gerekmektedir. Eğer reciver kapalı ise veya send işleminde sorun cıkarsa mesajınız kaybolmamalıdır. Bu ihtiyaçtan dolayı MQ transaction yapıyı desteklemektedir.
Bir Message nesnesi oluşturalım daha sonra MessageQueue nesnemize transaction ile Send() edelim.
[cs]
MessageQueueTransaction messageTransaction = new MessageQueueTransaction();
Message message = new Message();
message.Formatter = new System.Messaging.XmlMessageFormatter();
message.Label = smsLabel.ToString();
message.Body = smsBody;
messageTransaction.Begin();
try {
 smsQ.Send(message, messageTransaction);
 messageTransaction.Commit();
} catch (MessageQueueException) {
 messageTransaction.Abort();
 throw;
}
smsQ.Close();
return message.Id;
Şimdi bir kuyruğu dinleyelim ve yeni bir mesaj geldiğinde kuyruktan çekelim ve işledikten sonra tekrar bir sonraki mesajı bekleyelim. Böylelikle bir mesajı işlerken bu kuyruğa mesaj gönderen uygulama sonucuyu beklememektedir. Her iki uygulama arasında kurmamız gereken asenkron işlem mantığını sağlamış olduk.
[cs]
#region start stop listen q
public void StartListenQueue() {
 smsQ.ReceiveCompleted += 
  new ReceiveCompletedEventHandler(smsQ_ReceiveCompleted);
 smsQ.BeginReceive();
}
bool _isStopReceive;
void smsQ_ReceiveCompleted(object sender, ReceiveCompletedEventArgs e) {
 Message m = smsQ.EndReceive(e.AsyncResult);
 m.Formatter = new XmlMessageFormatter(
  new string[] { "System.String, mscorlib" });
 if (SMSQReceived != null)
  SMSQReceived(this, m);
 // bir sonrakini bekle
 if (!_isStopReceive) {
  smsQ.BeginReceive(); 
 }
}
public void StopListenQuese() {
 _isStopReceive = true; 
}
#endregion
Bugün: Asenkron programlama için MQ çok basit ve kullanışlı bir yoldur. Mevcut .net 2.0 teknolojisinde asenkron veri iletişiminde çözüm MS Message Queue haberleşmesidir.
Gelecekte: Bu yöntemde programcı tüm iş akışından sorumludur ve akışı adım adım izlemek gibi bir şansı yoktur. Yeni tüm programlar kendi başlarına çalışırlar. .net 3.0 ile hayatımıza girecek olan BPEL ile artık bu tür iş uygulamalarımızda tüm otomasyonu çalıştırmak bir üst programın sorumluluğunda olacaktır. BPEL bir workflow uygulaması tek farkı tüm işlemlerini WCF ile web servisler üzerinde yapmasıdır. WCF ise gene asenkron ve connectless iletişim için MSMQ kullanmaktadır.
İki ayrı sistem arasında MSMQ ile haberleşecekseniz tabii ki bir standart belirlemeniz gerekmektedir. Bir Dispacher nesnesini ve veri tanımlama standartı oluşturmanıza ihtiyacınız var.

12 Mayıs 2007 Cumartesi

Custom Data Source Control

Veri bağlana yeteneğine sahip çeşitli Web Server bileşenleri ile asp.net veri güdümlü mimariyi desteklemektedir. Veri bağlama işlemini veri sunucusu bileşenler (Data Source Controls) ve veri bağımlısı bileşenler (Data-Bound Controls) arasında gerçekleşmektedir. Data Source kontrolleri ilişkisel veri tabanı, dosya, stream nesnesi, iş nesnesi (Businnes Object) gibi her türlü veri kaynağını gösterebilirler. Data Source nesneleri (alta yatan veri kaynağından ve veri formatından bağımsız olarak) istikrarlı bir şekilde veri güdümlü nesnelere veriyi gösterirler.
Veri güdümlü yazılım mimarisinde sahip asp.net projelerinde çok fazla kullanılan iş nesneleri için iş nesnesini veri kaynağı olarak kullanan özelleştirilmiş Data Source kontrolleri yazmak sıkça kullanılan bir yoldur.

Özelleştirilmiş Data Source Control nasıl yazılır?

Run-time

CustomDataSource nesnemiz için iş kurallarını ortaya koyan temel sınıf System.Web.UI.DataSourceControl sınıfıdır. Özet (abstract) DataSourceControl sınıfını incelediğimizde bizden sadece GetView fonksiyonu beklediğini görüyoruz:
[cs]
protected abstract DataSourceView GetView(string viewName);
Burada ikinci bir nesneye ihtiyaç duymaktayız DataSourceView. Verinin run-time görünüşünü sağlamak görevi DataSourceView sınıfından türeteceğimiz sınıfa ait olacaktır. DataSourceView sınıfı da özet bir sınıftır. DataSourceView sınıfı üzerinde veri işlemlerini gerçekleştirmek için yeniden yazmamız gereken Execute fonksiyonları vardır.
[cs]
protected virtual int ExecuteDelete(IDictionary keys, IDictionary oldValues);
protected virtual int ExecuteInsert(IDictionary values);
protected internal abstract IEnumerable ExecuteSelect(DataSourceSelectArguments arguments);
protected virtual int ExecuteUpdate(IDictionary keys, IDictionary values, IDictionary oldValues);
Artık CustomDataSource yazabiliriz. Önce DataSourceControl sınıfından türetilmiş ve veri kaynağımıza ait özellikleri taşıyan asıl sınıfımız yazmalıyız. Daha sonra veri kaynağına ait görüntüyü ve işlemleri asıl sınıfta bulunan parametrelere göre yerine getirecek DataSourceView sınıfından türetilmiş yardımcı sınıfa ihtiyacımız var.
Örnek olarak event log üzerinde ki veriyi kullanan CustomDataSource geliştirelim.
Bilindiği üzere Event Log System.Diagnostics.EventLog sınıfı ile üzerine uygulama loglarının işletim sistemi yardımı ile kayıt edildiği genel loglarama yöntemidir. Çeşitli servisleri üzerinde barındıran bir makinenin Event Loglarını uzaktan inceleyebilmek işe yarar bir özelliktir.


EventLogDataSource sınıfı LogName ve LogSource özelliklerini kullanarak Event Log üzerinde ki verileri görüntüsünü GetView ile sağlamaktadır. İçeriğinde ki kodda olduk basit:
[cs]
#region override
protected override ICollection GetViewNames() {
 List ar = new List(1);
 ar.Add(_defaultViewName);
 return ar.ToArray() as ICollection;
}
/// 
/// Data soruce ait dataview eventlogdataview nesnesidir.
/// 
/// 
protected override DataSourceView GetView(string viewName) {
 // bu data sourse sadece bir view a sahipdir
 if (string.IsNullOrEmpty(viewName) || viewName == _defaultViewName)
  return EventLogView;
 return null;
}
private EventLogDataSourceView _eventLogView;

/// 
/// event log view
/// 
[Browsable(false)]
public EventLogDataSourceView EventLogView {
get {
 if (_eventLogView == null)
  _eventLogView = 
   new EventLogDataSourceView(this, _defaultViewName);
return _eventLogView;
}
}
#endregion

EventLogDataSource sınıfı tek bir view a sahip ve GetView fonksiyonu ile sahip olduğu view nesnesini döndürmektedir.
EventLogDataSourceView sınıfını ise Event Log içinde ki veri yönetmekle sorumlu ve başlarken belirlediğimiz gibi sadece Execute fonksiyonalrı ve gerekli özellikleri yeniden yazmış.
Event Log üzerinde ki log kayıtlarında Update veya tek bir kayıtı delete yapamazsınız bundan dolayı. EventLogDataSourceView CanDelete ve CancUpdate false değerlidir ve ExecuteDelete ve ExecuteUpdate fonksiyonlarında hata üretmektedir.
[cs]
protected override int ExecuteDelete(IDictionary keys, IDictionary oldValues) {
 throw new NotSupportedException("Event Log silme işlemi yapılamaz.");
}
ExecuteSelect fonsiyonu içerisinde Event Log EventLogDataSource parametrelerine göre okunmalı ve gerekli DataView geri döndürülmelidir. ExecuteInsert fonksiyonunda verilen veri Event Log içine eklenmelidir.
[cs]
/// 
/// EventSource ve EventLog özellikleri ile belirtilen event logları listeler
/// 
/// 
/// 
protected override IEnumerable ExecuteSelect(DataSourceSelectArguments arguments) {
 DataView dataView = null;
 Model.EventLogRow row;
 try {
  _table.Rows.Clear();
  System.Diagnostics.EventLog objEventLog = 
   new System.Diagnostics.EventLog(_owner.EventLogName);
  EventLogEntryCollection objEntries = objEventLog.Entries;
  foreach (EventLogEntry objEntry in objEntries) {
   if (_owner.EventLogSource == objEntry.Source) {
    row = _table.NewEventLogRow();
    row.Index = objEntry.Index;
    row.EntryType = objEntry.EntryType.ToString();
    row.TimeGenerated = objEntry.TimeGenerated;
    row.Source = objEntry.Source;
    row.Source = objEntry.UserName;
    row.MachineName = objEntry.MachineName;
    row.Message = objEntry.Message;
    _table.Rows.Add(row);
   }
  }
  dataView = new DataView(_table);
  if (_table.Rows.Count > 0)
   dataView.Sort = arguments.SortExpression;
 } catch (Exception) {}
 return dataView as IEnumerable;
}

/// 
/// event source ile belirtilen alana yeni bir log ekler
/// 
/// 
protected override int ExecuteInsert(System.Collections.IDictionary values) {
 System.Diagnostics.EventLog.WriteEntry(_owner.EventLogSource, 
  (string)values["Source"]);
 // en son entry no
 return (new System.Diagnostics.EventLog(_owner.EventLogName))
  .Entries.Count - 1;
}
Artık EventLogDataSource sınıfımızı iş başında görebiliriz Önce projemize ekleyelim gerekli parametreleri verelim ve çalıştıralım:

Design-Tim

Fakat design-time görüntüsü hiçte hoş durmamakta. EventLogDataSource sınıfımıza design-time görüntüsü de eklemeliyiz. Design-Time içinde benzer mantıkla bir Designer birde DesignerView sınıflarını yazmalıyız. Designer sınıfını EventLogDataSource sınıfının designerı olarak da atamalıyız.


[cs]
/// 
/// EventSource ve EventLog özellikleri ile belirtilen event logları listeler
/// 
/// 
/// 
protected override IEnumerable ExecuteSelect(DataSourceSelectArguments arguments) {
 DataView dataView = null;
 Model.EventLogRow row;
 try {
  _table.Rows.Clear();
  System.Diagnostics.EventLog objEventLog = 
   new System.Diagnostics.EventLog(_owner.EventLogName);
  EventLogEntryCollection objEntries = objEventLog.Entries;
  foreach (EventLogEntry objEntry in objEntries) {
   if (_owner.EventLogSource == objEntry.Source) {
    row = _table.NewEventLogRow();
    row.Index = objEntry.Index;
    row.EntryType = objEntry.EntryType.ToString();
    row.TimeGenerated = objEntry.TimeGenerated;
    row.Source = objEntry.Source;
    row.Source = objEntry.UserName;
    row.MachineName = objEntry.MachineName;
    row.Message = objEntry.Message;
    _table.Rows.Add(row);
   }
  }
  dataView = new DataView(_table);
  if (_table.Rows.Count > 0)
   dataView.Sort = arguments.SortExpression;
 } catch (Exception) {}
 return dataView as IEnumerable;
}

/// 
/// event source ile belirtilen alana yeni bir log ekler
/// 
/// 
[Designer(typeof(EventLogDataSourceDesigner))]
public class EventLogDataSource : DataSourceControl {
...
}
public class EventLogDataSourceDesigner : DataSourceDesigner {
...
public override DesignerDataSourceView GetView(string viewName) {
 if (viewName != _defaultViewName)
  return null;
 if (_view == null)
  _view = new EventLogDesignerDataSourceView(this, viewName);
 return _view;
}
}
/// 
/// design-time event log data source view
/// 
public class EventLogDesignerDataSourceView : DesignerDataSourceView {
...
public override IDataSourceViewSchema Schema {
get {
 return (new TypeSchema(typeof(Model.EventLogDataTable))).GetViews()[0];
}
/// 
/// design-time gösterimi için data getir
/// 
/// 
/// 
/// 
public override IEnumerable GetDesignTimeData(int minimumRows, out bool isSampleData) {
 isSampleData = true;
 Model.EventLogDataTable table = new Model.EventLogDataTable();
 DataView dataView = null;
 ...
 // EventLogDataView içinde olduğu gibi EventLog okunur
 // eğer veri yoksa örnek veri oluşturulur 
 if (table.Rows.Count == 0)
  GenerateSampleData(table);
 dataView = new DataView(table);
 return dataView as IEnumerable;
} 

private void GenerateSampleData(Model.EventLogDataTable table) {
 Model.EventLogRow row;
 table.Rows.Clear();
 for (int i = 1; i < 11; i++) {
  row = table.NewEventLogRow();
  row.Index = i;
  row.EntryType = "Sample Entry Type";
  row.TimeGenerated = DateTime.Today;
  row.Source = "Sample Source Type";
  row.UserName = Environment.UserName;
  row.MachineName = Environment.MachineName;
  row.Message = "Event Log Source and Event Log Name properties required to setup.";
  table.Rows.Add(row);
 } 
}
}
.
Burada küçük bir üçkâğıt var. Normalde EventLogDesignerDataSourceView sınıfa şema özelliği içinde IDataSourceViewSchema ara yüzüne sahip bir sınıf ile uzunca kullanılan verinin şema bilgisini anlatmanız gerekmektedir. Ben bir Typed DataSet içinde kullandığım veriyi modelledim ve şema bilgisini bu model üzerinden ürettim. Böylelikle CustomDataSource konusunun sonuna geldik. İşte design-time görüntümüz.

11 Mayıs 2007 Cuma

XML Validation

XML veri doğrulaması nedir? XML veri doğrulamasını koddan bağımsız olarak nasıl yaparız? DataRow nesnesini nasıl Serialise Deserialise yaparız?


Farklı platformlar arasında veriyi taşımak için taşınan verinin modelini de bilmek gerekmektedir. XML veriyi ve veri modelini bir arada saklanması ve taşınması mantığından ortaya cıkmış bir standarttır. XML veri modelleme ve doğrulama için iki geçerli yolumuz var. Biri çok eski bir standart olan DTD standardıdır. Diğeri web servisler ile adını duyuran XSD standardıdır.
XSD (XML Shema Definition) XML veri modelin doğrulanma ihtiyacından doğmuş olan ve ilk Microsoft tarafından duyurulan bir standarttır. Bilindiği üzere Typed DataSet sınıfları TypedDataSetGenerator sınıfı ile XSD dosyaları kullanılarak üretilen nesnelerdir.
DTD (Data Type Definition) XSD benzeri bir XML tanımlama standardıdır. Avantajı XSD ye göre çok basit olması.  Eğer hız her şey ise sizin sisteminizde DTD veri doğrulamak için uygun bir  seçim olabilir. Özellikle eski sistemlerde (web servislerden önce) sıkça kullanılırdı. Eski platformlarda sadece DTD verisini test eden sınıflarımız vardı.  Eğer sizde benim gibi işiniz gereği eski platformları kullanan sistemler ile haberleşmek zorunda kalırsanız DTD doğrulama için Dispatcher sınıfı size yardımcı olacaktır.
XML veriyi kontrol etmek için önce XmlResolver nesnesine veri doğrulama kaynağını vermemiz gerekmektedir. Bu uygulamada veri doğrulama kaynağını koddan bağımsız hale getirmek için veri doğrulama bilgisini dosyadan okuyacağız. Dosyadan okunan veri doğrulama bilgisi için önce XmlResolver sınıfından türetilene ve veri doğrulama şablonunu yerel dosyadan okuyacak bir sınıf yazmalıyız.
[cs]
#region local document type resolver
/// 
/// XML Resolver için uygulama sınıfı
/// 
internal class LocalDocumentTypeResolver : XmlUrlResolver,IDisposable {
 FileStream stream = null;
 public LocalDocumentTypeResolver(String systemEntry) {
  stream = new FileStream(systemEntry,
   FileMode.Open,
   FileAccess.Read,
   FileShare.Read);
 }
 override public object GetEntity(Uri absoluteUri, 
  string role, Type ofObjectToReturn) 
 {
  return stream;
 }
 
 #region IDisposable Members
 // xsd kullanırken data source olarak result gösterilirse file used kalıyor
 // onun için LocalDocumentTypeResolver IDispose olmak zorunda
 public void Dispose() {
  stream.Dispose();
  stream = null;
  GC.Collect();
 }
 #endregion
}
#endregion
Artık yerel dosyalardan doğrulama bilgisini okuyabildiğimize göre XML verileni bu dosyalara göre doğrulayabiliriz.
[cs]
/// 
/// verilen mesaja ve tip tanımlama dosyasına gre veri kümesi üretir
/// 
/// ayrıştırılacak mesaj        
/// veri kontrolünü sahlayacak danımlama dosyası tam yolu
/// mesajın root element
/// doğrulama yapılacak dosya uzantısı dtd veya xsd olmalıdır
/// Doğrulama dosyası xsd veya dtd değil
/// tanımlama dosyasına göre oluşturulmuş veri kümesi
public static System.Data.DataSet Dispacth(string message,string validateFilePath,string rootElement){
        if (string.IsNullOrEmpty(rootElement))
                throw new ArgumentException("rootElement", 
                       "XML doğrulaması yapılacak xml root element verilmelidir.");
        if (!Regex.IsMatch(validateFilePath.ToLower(), @"(.)*\.dtd$|(.)*\.xsd$"))
                throw new ArgumentOutOfRangeException("validateFilePath", 
                        "XML doğrulama dosyası 'dtd' veya 'xsd' uzantılı olmalıdır.");
        DataSet result = new DataSet();
        using(LocalDocumentTypeResolver fileResolver =new LocalDocumentTypeResolver(validateFilePath)){ 
                XmlReaderSettings xmlSetting = new XmlReaderSettings();
                if (Regex.IsMatch(validateFilePath.ToLower(), @"(.)*\.dtd$")) {
                        if (message.IndexOf("" + message;
                        xmlSetting.ValidationType = ValidationType.DTD;
                } else if (Regex.IsMatch(validateFilePath, @"(.)*\.xsd$")) {
                        xmlSetting.Schemas.Add(null, XmlTextReader.Create(validateFilePath));
                        xmlSetting.ValidationType = ValidationType.Schema;
                }
                StringReader textReader = new StringReader(message);
                XmlTextReader xmlTextReader = new XmlTextReader(textReader);
                xmlTextReader.XmlResolver = fileResolver;
                xmlSetting.ValidationEventHandler += 
   new ValidationEventHandler(xmlSetting_ValidationEventHandler);
                XmlReader xmlReader = XmlReader.Create(xmlTextReader, xmlSetting);
                isSuccess = true;
                result.ReadXml(xmlReader);
                if (!isSuccess)
                        result = null; 
        } 
        return result;
}
static void xmlSetting_ValidationEventHandler(object sender, ValidationEventArgs e) {
        isSuccess = false;
}
Yukarıda ki kod ne yapıyor: message ile verilen XML verisini validateFilePath konumunda bulunan DTD veya XSD uzantılı dosyayı LocalDocumentTypeResolver sınıfı kullanarak doğruluyor. XmlReader sınıfına XSD için kullanılacak şema eklemek gerekmektedir. DTD için ise şema bilgisi xml verinin en başında tanımlanmalıdır.
Uygulama kodu ise oldukça sadedir.
[cs]
private System.Data.DataTable Dispatch() {
        string xml = txtXML.Text;
        string shemaFile = optDTD.Checked ? DTDFile : XSDFile;
        string rootElement = txtSchema.Text
  .Substring(0, txtSchema.Text.IndexOf(Environment.NewLine))
                .Split(' ')[1];
        SaveSchemaFile(shemaFile);
        System.Data.DataSet result = 
  Framework.Dispatcher.Dispacth(xml, shemaFile, rootElement);
        return result.Tables[0];
}
Dispatch işleminde bize gereken bir diğer işlev ise Serializable DataRow nesnesidir. Yani önce test edilecek veriyi oluşturmak gerekmektedir. DataRow sınıfı varsayılan yapılandırıcıya sahip olmadı için hiçbir şekilde Serialize haline getirilemez. Çözüm DataRow nesnesini DataTable içine alıp serialize edilmektir.
[cs]
/// 
/// DataRow xml olrak serialize eder
/// 
/// 
public static string SerializeRow(DataRow row) {
    Type tableType = row.Table.GetType();
    DataTable table = (DataTable)Activator.CreateInstance(tableType);
    DataRow newRow = table.NewRow();
    newRow.ItemArray = row.ItemArray;
    table.Rows.Add(newRow);
    StringWriter writer = new StringWriter();
    table.WriteXml(writer);
    return writer.ToString().Replace("xml:space=\"preserve\"", "");
}

/// 
/// xml string üzerinden DataRow üretir
/// 
/// xml data source
/// data table type
public static DataRow DeseriazeRow(string xmlDataSource, Type tableType) {
    StringReader reader = new StringReader(xmlDataSource);
    DataTable table = (DataTable)Activator.CreateInstance(tableType);
    table.ReadXml(reader);
    return table.Rows[0];
}
Bir biri ile ayrı uzaylarda çalışan sistemler eğer web servisi gibi bir teknoloji kullanmadan haberleşiyorsa Dispatcher gibi bir çözüme gereksinim duyarlar. DTD ile temel XML doğrulama özellikle eski sistemler ile yapılan çalışmalarda çok fazla karşımıza çıkmaktadır.

31 Mart 2007 Cumartesi

Generic Oracle DataAccess Layer + Typed DataSet

Data Access Layer için manüel kodlama yapar mısınız? Yapıyorsanız hep aynı kodları yazmaktan sıkılmadınız  mı ? :) (Bulduğunuz çözüm Code Generator yazmak mıdır? Dilin nimetlerinden yararlanın derim.) SQL Server ortamında Data Access Layer için en rahat çözüm tabii ki Typed DataSet yani XSD dosyalarıdır. Typed DataSet'ler üzerinde TypedDataSetGenerator ile çok kolay bir şekilde veri tabanı üzerindeki Store Procedure, Table, View gibi nesnelere ulaşıp gerekli olan kod bloğunu üretebilirsiniz. Nasıl yapılacağını buradan öğrenebilirsiniz. 
SQL Server platformunda Typed DataSet kullanmaya alışmış ve tüm alt yapısını SQL Server üzerine kuran bir yazılımcı Oracle Platformuna geçince ne olur? Bir süre sudan cıkmış balık durumda kalır. :) En büyük veri tabanı üreticilerinden biri olan Oracle'ın .Net platformu için SQL Server kadar hızlı ve kolay bütünleşik bir kod geliştirme aracı ne yazık ki yok. VS içinde ki Server Explorer penceresinden Oracle Packet içeriklerini bile göremiyorsunuz. Odp.Net ile gelen Oracle Explorer penceresi ile ancak Oracle veri tabanınızın detaylarını görebilmektesiniz. Fakat el alışkanlığı ile burada ki nesleri sürükleyip Xsd dosyanıza bırakmak istediğinizde nesnelerin taşınamaz olduğunu görürsünüz. Google'da Oracle Typed DataSet üzerine derin araştırmalar yapıp en sonunda Oracle XSD dosyalarını bir sonra ki VS sürümünde destekleyeceğini açıklayan duyurusu ile tüm hayalleriniz yıkılır. Ama iş beklemez çözüm bulunmalı. Peki, Oracle üzerinde Typed DataSet için 'minimum kodlama ile' Data Access Layer nasıl olacak ki?
Önce DataAccesss Layer nasıl olacak? Typed DataSet kullanan programlarda çalışma mantığı: Veriyi çek, kullanıcıya işlettir ve veri tabanını System.Data.DataAdapter sınıfı ile güncelle şeklindedir. Typed Data Set kullanıyorsanız DataAccess Layer System.Data.DataAdapter nesnesinin bir uygulamasını içermek zorundadır. SQL platformunda bunu TypedDataSetGenerator yaptığı için problem yok. Oracle platformu için çözüm üretelim:

İş ihtiyacımız nedir? Oracle Packetler içinde olan Store Procedure'leri kendi projemiz içinde çağırabilmek ve Typed DataSet üzerinde kullanmak. (Zaten diğer Oracle nesnelerine VS içinden erişilmekte.) Tüm bunları sürekli aynı kodları tekrar etmeden yapmalıyız.

Çözüm: Kod tekrarı yapmamak için Generic Data Access Layer oluşturmak.  İhtiyacımız olan tüm Store Procedure'leri Typed DataTable sınıflarına attribute olarak atamak ve Generic Data Access Layer üzerinde bu attributeleri kullanarak gerekli işlemleri yerine getirmek.(Bir önceki konuda olduğu gibi bu konuda da Attribute sınıflar yardıma koşuyor.) Her zaman ki gibi kodu en üst seviyede yazmalıyız. Sürekli kendisini tekrar eden kodlama yerine en üst seviyede bir defa kod yazmak. Böylece DataAdapter nesnesine olan bağımlılı tek yerden ve kesin bir şekilde ortadan kaldırabiliriz.

Önce Store Procedure'leri tanımlayacağımız Attributeleri yazalım:

 Store Procedure Attribute sınıflarımızı Typed DataTable sınıflarımıza ekleyelim.
[cs]
[SelectProcedure("ACME.PCK_TEST.PRC_GET_ALL_TEST", "refcursor P_TEST:", DefaultSelect = true ) ] 
[SelectProcedure("ACME.PCK_TEST.PRC_GET_TEST_BY_ID","ID1",DefaultSelect=false)] 
[InsertProcedure("ACME.PCK_TEST.PRC_INSERT_TEST", "NAME1", "VALUE1", "out P_ID1 : Int32 id1")] 
[UpdateProcedure("ACME.PCK_TEST.PRC_UPDATE_TEST", "NAME1","VALUE1","ID1")] 
[DeleteProcedure("ACME.PCK_TEST.PRC_DELETE_TEST","ID1")] 
partial class TEST1DataTable 
{ 

}
Neden birden çok Select Procedure var?  DataAdapter nesnesi her bir kayıtın RowState özelliğine göre kendisine ait InsertCommand, UpdateCommand, DeleteCommand fonksiyonlarında biri kullanılarak veriyi güncelleyecektir. Bu sebebten dolayı sadece birer tane Insert, Update ve Delete Store Procedure eklenmesine izin verilmiştir. Birden cok SelectCommand gerekecektir.
Böylece artık Typed DataTable nesneleri Oracle Packet üzerinde ilişkili oldukları Store Procedure'leri bilmektedir. DataTable nesneleri için artık bir Generic DataAdapter nesnesileri oluşturabiliriz. Veri şemasını biliyoruz, veriyi veri tabanında işleyecek Store Procedure'leri biliyoruz o halde Data Access Layer üzerinde sürekli aynı kodları yazmanın gereği var mıdır? En tepeye Generic bir Data Access Layer yazar ve tüm Logic içerisinde bu sınıfı kullanırız. (İp ucu: Yerinizde olsam Base Logic sınıflarınıda Generic yapardım. Bu sayede bütünleşik ve tamamen Generic bir çatınız olur. Projelerinizi minumum kodlama ile bitebilirsiniz.
IDataService ile genel veri tabanı etkileşim ihtiyaçları ortaya konmuştur.

Bu işlemleri gerçekleştiren OracleDataService sınıfıdır ki kendisi projede tek başına Data Access Layer olmaktadır. OracleDataService sınıfı kendisine tip parametresi olarak verilen Typed DataTable nesnesinin StoreProcedure Attribute'lerini almakta ve Odp.Net ile gelen OracleDataAdapter nesnesini bu Attribute'lere göre oluşturmaktadır.
[cs]
StoreProcedureAttribute[] sps = (StoreProcedureAttribute[])table.GetType()
 .GetCustomAttributes(typeof(StoreProcedureAttribute), false); 
foreach (StoreProcedureAttribute att in sps) 
{ 
 if (att is InsertProcedure) 
  InitInsertProcedure(att.ProcedureName, att.Paramters); 
 else if (att is UpdateProcedure) 
  InitUpdateProcedure(att.ProcedureName, att.Paramters); 
 else if (att is DeleteProcedure) 
  InitDeleteProcedure(att.ProcedureName, att.Paramters); 
 else if (att is SelectProcedure) 
  InitSelectProcedure(att.ProcedureName, att.Paramters,
   (att as SelectProcedure).DefaultSelect ); 
 else 
  InitCustomProcedure(att.ProcedureName, att.Paramters); 
}
Odp.Net OracleDataAdapter nesnesine ait Commandları oluştur:
[cs]
private Oracle.DataAccess.Client.OracleCommand 
  InitStoreProcedure(string procedureName, string[] parameters) 
{ 
 Oracle.DataAccess.Client.OracleCommand command = 
  new Oracle.DataAccess.Client.OracleCommand(); 
 command.Connection = _connection; 
 command.CommandType = System.Data.CommandType.StoredProcedure; 
 command.CommandText = procedureName; 
 foreach (string parameter in parameters) 
 {
  command.Parameters.Add(InitParameter(parameter)); 
 } 
 return command; 
}
Kullanımı gayet basittir:
[cs]
Demo.Framework.Generic.Data
 .OracleDataService 
_dataAccessLayer = new Demo.Framework.Generic.Data
 .OracleDataService(); 
public Form1() 
{ 
 InitializeComponent(); 
 _dataAccessLayer.FillAll(dataSet1.TEST1); 
} 

private void tEST1BindingNavigatorSaveItem_Click(object sender, EventArgs e) 
{ 
 _dataAccessLayer.UpdateTable(dataSet1.TEST1); 
}

Değişken parametrele Store Procedure çağrılarını gene OracleDataService sınıfını kullanarak yapabilirsiniz.

Sonuç

Tüm Data Access çağrıları sürekli aynı kodların tekrarı olmaktadır. Genelde manüel kodlama ile veya Code Genetor yazmak ile  bu sıkıcı kod bloğu oluşturulmaktadır. Yukarıda ki yaklaşım ile tekrar edilen kodlardan kurtulmuş olduk. Oracle ve Typed Dataset kullanan uygulamalar için genel (ve tecrübe ile sabit yeterli) bir Data Access Layer yazmış olduk. Aynı yaklaşımı SQL üzerinde de kullanabilirsiniz hatta sadece isim uzayını ve bir iki satır kodu değiştirerek aynı sınıfı kullanabilirsiniz. Fakat ben bunun yerine SQL ortamında size doğrudan XSD dosyalarını kullanmanızı öneririm.

30 Mart 2007 Cuma

Kendi Kendini Doğrulayan Typed DataRow

Verinin kendi kendini test etme yaklaşımı Validation Application Block ile iyice güçlendi. Artık Entity’ler daha zeki ve sakladıkları verinin doğruluğundan Entity nesnesi sorumlu. Validation Application Block veriyi Entity üzerinde saklayan programlar için mükemmel bir yaklaşım sunuyor. Fakat Typed DataRow üzerinde bir Application Block yok. Peki benim gibi üşengeç bir .Net yazılımcısı iseniz Entity nesneleri ile ilişkisel verileri saklamak zor geliyorsa, tüm verileri Typed DataSet içinde taşımak ve işlemek size daha kolay geliyorsa ne yapmalısınız?
Seçenek kalmadı kullandığınız Typed DataSet’leri kendi kendini test edebilir şekle getirmeliyiz. Ne gerekli bize:
  1. Atomik test işlemlerini yapan Attribute sınıflar.
  2. Bu test işlemlerini işletecek Controller sınıfı
Tabi tüm bu sınıfların veriden bağımsız olması yani Generic olması gerekmektedir. Burada ki kısıtlama Validation Application Block her bir Entity Field’a attribute vererek doğrulama yapılmakta fakat Typed DataRow üzerinden ki Field’lara Attribute kalıcı olarak eklenemez. Onun için bizim CheckAtiibute’lerimiz direk Typed DataRow nesnelerine eklenmelidir.
Doğrulama iki şekilde olur:
  1. Check: Veri kendi başına doğru mu sorusudur. Mesela email alanı gecerli mi? Para alanına negatif değer girildi mi?
  2. Validation: Veri iş kurallarına uygun mu sorusudur. Yani veri diğer veriler ile birlikte doğru mu?

Tüm işi gerçekleştiren Controller sınıfıdır. Check hatalarını girildiği anda (OnFly) yakalamak için DataSource almaktadır. DataSource üzerinden veri değiştiği zaman BaseCheck sınıfından türetilen Check kuralları çalıştırılmakta ve karşılaşılan hatalar ColumnError olarak atanmaktadır.

Önce takip edilecek Row’a Attributeler atanmalı: 
[cs]
[NotNullCheck("Column1",UserFriendlyName="Birinci alan")]
[MinimumCheck("Column2",12, UserFriendlyMessage = "İkinci alandaki veri 12 den küçük olamaz")] 
[UniqueCheck("Column3", UserFriendlyMessage = "İkinci alandaki veri tüm tabloda unique olmalı")]
partial class DataTable1Row 
{   

}
Veri değişimi takibi:
[cs]
#region FollowTable 
protected virtual void FollowDataSource(DataTable table) 
{ 
 table.ColumnChanging += 
  new DataColumnChangeEventHandler(table_ColumnChanging); 
} 

protected virtual void table_ColumnChanging(object sender, DataColumnChangeEventArgs e)
{ 
 // veri değişti check kurallarını işlet 
  if( e.Row.RowState != DataRowState.Deleted) 
  e.Row.SetColumnError(e.Column, 
   CheckRow(e.Column, e.Row, e.ProposedValue)); 
} 
#endregion
Değişen veriyi test et:
[cs]
#region check 
///  
/// kolon verisi değişti kontrolü 
///  
/// değişen kolon 
/// kullanılan row 
/// yeni değer 
/// hata iceriyorsa hata mesajı. hata yoksa boş string 
protected virtual string CheckRow(DataColumn dataColumn, DataRow dataRow, object columnNewValue) 
{ 
 dataRow.EndEdit(); 
 string result = string.Empty; 
 BaseCheck[] attrs = (BaseCheck[])dataRow.GetType()
  .GetCustomAttributes(typeof(BaseCheck), true); 
 foreach (BaseCheck attr in attrs) 
 {  
  if (attr.ColumnName == dataColumn.ColumnName) 
  { 
   result += attr.IsValid(dataRow, columnNewValue); 
  } 
 } 
 return result; 
} 
#endregion 
Check işleminin çalışma zamanı görüntüsü:

IsValid çağrısı ile önce Check kontrolleri çalıştırılır eğer tüm checkler doğru ise kontrol edilen Row HasSelfValidation attribute’ne sahip mi kontrolü yapılır. Eğer Row HasSelfValidation attribute’ne sahip ise SelfValidation attribute’ne sahip fonksiyonlar aranır ve bu fonksiyonlar çağırılır.
[cs]
#region validate 
///  
/// Veri tabanına kayıt etmeden önce kayıtın doğruluğunu kontrol et 
/// hata bulunur ise hatayı throw et 
///  
/// kontrol edilecek kayıt 
public virtual bool IsValid(DataRow row) 
{ 
 row.EndEdit(); 
 _errorMessage = string.Empty; 
 if (row.RowState != DataRowState.Deleted 
  && row.RowState != DataRowState.Detached) 
 { 
  foreach (DataColumn col in row.Table.Columns) 
   _errorMessage += CheckRow(col, row, row[col]);                       
   //tüm childe row'ları valid mi 
  foreach (DataRelation relation in row.Table.ChildRelations) 
   foreach (DataRow childeRow in row.GetChildRows(relation)) 
    foreach (DataColumn col in childeRow.Table.Columns) 
     _errorMessage += 
      CheckRow(col, childeRow, childeRow[col]); 
 } 
 // validation 
 if (_errorMessage.Length == 0 && row.RowState != DataRowState.Deleted 
  && row.RowState != DataRowState.Detached) 
 { 
  HasSelfValidation[] attrs = (HasSelfValidation[])row.GetType()
   .GetCustomAttributes(typeof(HasSelfValidation), false); 
  if (attrs != null && attrs.Length > 0) 
  { 
   foreach (MethodInfo method in row.GetType().GetMethods()) 
   { 
    SelfValidation[] validation = (SelfValidation[])method
     .GetCustomAttributes(typeof(SelfValidation), false); 
    if (validation != null && validation.Length > 0) 
    { 
     object result = method.Invoke(row, new object[] { }); 
     if (!string.IsNullOrEmpty((string)result)) 
     { 
      this._errorMessage += (string)result; 
     } 
    } 
   } 
  } 
 } 
 return string.IsNullOrEmpty(_errorMessage); 
} 
#endregion 
Validation kontrollerinin çalışma zamanı görüntüsü: