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.