Burak Selim Senyurt(MVP)
Matematik Mühendisi bir .NET Severin Yazıları...

Screencast - AJAX Enabled WCF Services

Perşembe, 4 Mart 2010 10:45 by bsenyurt

Merhaba Arkadaşlar,

Görsel derslerimize kaldığımız yerden devam ediyoruz. Bu sefer elimizdeki materyaller bir WCF Service, Asp.Net Web Uygulaması ve AJAX. Bunları bir arada düşündüğümüzde ise karşımıza AJAX Enabled WCF Service kavramı çıkıyor. Bildiğiniz üzere Asp.Net uygulamalarında AJAX imkanları kullanılabilmekte ve bu sayede kısmi olarak post işlemleri gerçekleştirilebilmekte. Çok basit anlamda bir sayfanın tamamını sunucuya göndermeden sadece istenilen parçaların gönderilmesi ve sonuçlarının ele alınabilmesi mümkündür. Tabi sonuçların istemci tarafında ele alınması gerekmekte(Örneğin Javascript ile). AJAX Destekli WCF Servislerinde ise herhangibir servis operasyonunun çağrısı sırasında, servisi çağıran web sayfasının tamamının sunucuya gönderilmemesi imkanı kazanılmaktadır. AJAX Destekli WCF Servislerinin kullanıldığı pek çok senaryo söz konusudur. En basit haliyle otomatik metin tamamlama kabiliyetine sahip kontroller için bu teknikten yararlanılabilir. Bizde görsel dersimizde konu olarak bebek adlarını(Baby Names) ele almaya çalışacağız. Upss!!! Bebek Adları mı? Wink İzleyelim ve görelim.

Video Boyutu : 24 Mb

Süre : 14:06

Download Etmek veya İzlemek için

WebApplication2.rar (22,09 kb)

WCF Eco System

Salı, 5 Ocak 2010 10:30 by bsenyurt

Merhaba Arkadaşlar,

Özellikle son bir iki yıllık zaman dilimi içerisinde .Net tarafında pek çok servis modeli ve ismiyle karşılaştık. Örneğin Astoria kod adıyla başlayan Ado.Net Data Services, Silverlight gibi Rich Internet Application' ları hedef alan .Net RIA Services vb... (Eğer Microsoft' un ürünleri için kullandığı kod adlarını merak ediyorsanız Wikipedia' daki ilgili listeye bakmanızı öneririm) Hal böyle olunca ortada bir sürü kod adı ve isim oluşmaya başladı. Buda çok doğal olarak bizim gibi geliştricilerin kafasında pek çok soru işaretine neden oldu. Acaba hangi servis modelini hangi amaçlar ile kullanmalıyız? Bunların nihai sürümler yaklaştıkça isimlendirmeleri neler olacak? Ne gibi avantaj veya dez avantajları var?

Soruları arttırmak mümkün. Aslında kabul edilmesi gereken önemli bir nokta var; Tüm bu servis modelleri .Net Framework 3.0' dan beri var olan ve her sürümde önemli yetenekler kazanan Windows Communication Foundation(WCF) alt yapısı(Infrastructure diyebiliriz) üzerinde konuşlandırılmış durumda. Şu an içinde bulunduğumuz servis modellerinin çeşitliliğini ve sayısını düşündüğümüzde ise bir Eco System' in oluştuğunu net bir şekilde ifade edebiliriz. Aşağıdaki tabloda WCF Eco System' in parçaları yer almakta olup kısaca amaçları özetlenmeye çalışılmaktadır.

Model

Özet Bilgi

SOAP Services Modeli

Interoperability standartlarına uygun olan böylece örneğin Java gibi platformlar ile konuşabilen, mesaj tabanlı güvenliği(Message Based Security) baz alabilen, transaction akışına(Transaction Flow) izin veren, IIS üzerinden HTTP tabanlı veya IIS dışından host edilebilen(örneğin bir Windows Service, Windows Forms veya WPF uygulaması yada basit bir Console programı olarak), pek çok WS-* standardını destekleyen tipteki servisler olarak düşünülebilir. Bu servis modeli .Net Framework 3.0 versiyonundan bu yana mevcuttur. Geliştiricilere çok daha fazla kontrol imkanı sunan bir model olarak düşünülebilir. Diğer servis modellerindeki gibi belirli bir konuya odaklanmaktan ziyade ihtiyaçlara göre düşünülen çözümlerde değerlendirilir.

WebHttp Services Modeli

URI(Uniform Resource Identifier) bazlı olaraktan servis operasyonlarının RESTful yaklaşımına göre sunulduğu fonksiyonellikleri barındıran servis modelidir. WCF tarafındaki bu yetenekler .Net Framework 3.5 ile birlikte gelen Web programlama modeli(Web Programming Model) sayesinde ortaya çıkartılmış olup .Net Framework 4.0' da ek özellikler ile arttırılmıştır. Bu modelde verinin Get,Post,Put ve Delete gibi HTTP protokol metodlarına uygun olaraktan sunulması mümkündür. Geliştiriciler verinin sunulması sırasındaki URI bilgisine, çıktı formatına(örneğin JavaScript Object Notation tipinden olması) müdahalede bulunabilir.

Data Services Modeli

Veri modelimizi(Data Model) ve bu modelin içerdiği iş mantığın bir RESTful arayüzü üzerinden sunmak istediğimiz durumlarda kullandığımız servis modeli yaklaşımıdır. .Net için Open Data Protocol desteğini de içermektedir. Aslında ilk olarak .Net Framework 3.5 Service Pack 1 ile ve Ado.Net Data Services adıyla ortaya çıkmıştır. Bu yaklaşımda servisin sunacağı veri kaynağına ulaşırken Ado.Net Entity Framework gibi gelişmiş ORM birimlerinden yararlanılabilir. Ancak istenildiğinde Custom LINQ Provider' lardanda faydalanılıp farklı veri kaynaklarının kullanılması mümkün olabilir.

* RIA Services ile Data Services zaman zaman bir birlerine karıştırılmaktadır. Aslında Data Services, veri modelinin ve bu model ile ilişkili iş mantığının sunulması ile ilgilenmekte iken RIA Service' leri Silverlight uygulamalarının end-to-end modelinde geliştirilmesine odaklanmaktadır. Bu nedenle benzer olmalarına rağmen ilgilendikleri vakalar tamamen farklıdır.

** Yine Data Service' lerin zaman zaman WebHttp Service' leri ile karıştırılmasıda söz konusudur. Ancak yine her iki servis modelinin ilgilendiği vakalar farklıdır. WebHttp Service' leri URI, format ve protocol bilgileri üzerinde tam yönetime izin verecek şekilde RESTful servisler geliştirebilmemize olanak sağlar. Ancak Data Service' lerde RESTful arayüzü zaten hazırdır ve sadece veri modeli ile ilişkilendirilmesi yeterlidir.

Workflow Services Modeli

Uzun süreli(Long Running) ve kalıcılık(Persistence) desteği olması gereken Workflow uygulamalarının servis bazlı olarak kullanılabilmesini sağlayan modeldir. Bu modele göre Workflow nesnelerinin WCF servisi olarak sunulması, tüketilmesi ve ayrıca Workflow nesnelerinin içerisinde WCF servislerinin çağrılıp değerlendirilmesi mümkündür.

* .Net Framework 3.0' da birbirlerine uzaktan bakan WF ve WCF alt yapıları, .Net Framework 3.5 ile flört etmeye başlamış ve .Net Framework 4.0' da evlenmiştir. Wink

RIA Services Modeli

Sliverlight gibi Rich Internet Application(RIA) uygulamalarında orta katmandaki iş modelinin servis bazlı olarak hem istemci hemde sunucu tarafında yönetilmesini, oluşturulmasını ve kullanılmasnı kolaylaştıran end-to-end servis modelidir. Önceki adı .Net RIA Services olmasına rağmen değişen adıyla birlikte tam olarak Silverlight 4.0 sürümünde nihai versiyonuna ulaşacağı tahmin edilmektedir.

Tabi buradaki bilgiler dışında aşağıdaki çizelgeyide göz önüne almamızda yarar vardır. Bu çizelgede WCF Eco System' in mimari modeli gösterilmektedir.

Aslında WCF alt yapısı üzerinde duran servis programlama modelleri, geliştiricilerin vakaya göre WCF alt yapı detaylarından uzaklaşmasını sağlamaktadır. Bu, özellikle Data Services ve RIA Services modellerinde ön plana çıkmaktadır. Yine de istenildiğinde bu modelleri esnetebileceğimizi bilmeliyiz. Hatta bana kalırsa WCF alt yapısı üzerine oturan kendi programlama modellerimizi de geliştirebiliriz. Ancak buna gerek olup olmadığını tartışmalıyız.

Umarım WCF Eco System hakkında biraz fikir sahibi olabilmişizdir. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

WCF Known Types Analizi

Salı, 20 Ekim 2009 22:36 by bsenyurt

Merhaba Arkadaşlar,

Bilindiği üzere WCF aslında SOA(Service Oriented Architecture) mimarisinin uygulama modellerinden birisidir. İşin içerisinde servisler söz konusu olduğunda ağlar ve sistemler arası mesajlaşlamalar söz konusudur. Mesajlaşmalar söz konusu olduğundaysa, servis ve istemci arasında hareket eden verinin serileşebilir olması önem arz eden konuların başında gelmektedir. Ne varki serileşen veri içeriklerinin, platform bağımsızlık adına her iki tarafında kullanabileceği tiplerden(Types) oluşmasının sağlanması bir avantajdır. İşte bu noktada biz WCF geliştiricileri için anlaşılması zor olan ve dikkatle üzerinde durumlası gereken kıyıda köşede kalmış konulardan biriside Known Types kavramıdır.

WCF ile birlikte gelen ve serileştirmede kullanılan tipler esas itibariyle Shared Contracts kategorisindendir. Nitekim serileşen tipin ve kullandığı içeriğin Interoperabilitiy kuralları çevresinde değerlendirilebiliyor olması gerekir. DataContractSerializer, JsonDataContractSerializer ve XmlSerializer gibi serileştirici tipler bu kategoride yer almaktadır. Diğer yandan BinaryFormatter, SoapFormatter veya NetDataContractSerializer gibi tipler, Shared Types kategorisinde yer alan serileştiricilerdir. Genellikle serileşen tiplerin içerdiği tip bilgilerinin tanımlandığı Assembly' lar, uygulama ile aynı makinede yer alır. Örneğin serileştirilebilir tipin içeriğinin .Net CLR tiplerinden oluştuğunu düşünelim. Bu durumda ters serileştirme işlemini üstelenen uygulamanında bu CLR(Common Language Runtime) tiplerini biliyor olması, bir başka deyişle uygun Framework Assembly' larına sahip olması yeterlidir. Ancak SOA tabanlı bir sistemde serileştirme(Serialize) ve ters-serileştirme(Deserialize) yapan tarafların aralarında taşıdıkları tiplerle ilişkili olarak ortak bir noktada buluşmaları gerekmektedir. Nitekim hem sağlayıcı hemde tüketici taraflar kendi platformlarında kendi özel veri tiplerine sahiptir ve sadece bu tipleri kullanabilir. Bu noktada tipin XSD(XmlSchemaDefinition) şemaları içerisinde tanımlandığını(dolayısıyla serileşen paketler içerisinde taşındığını) ve örneğin Proxy' yi üreten tarafta bile kullanılabilecek bir tipe karşılık gelmesi gerektiğini söyleyebiliriz. Tabiki buradaki veri tipi bilgileri arada transfer edilen XML içeriklerinde ortaktır.

Aslında bu teorik bilgiler, yazarken bile insanın kafasını allak bullak etmeye neden olabilir. Bu yüzden kafamızda sorunsal haline gelebilecek bu konuyu örnekler üzerinden açıklamakta yarar olacağı kanısındayım. İşe ilk olarak Visual Studio 2008 ortamında oluşturulumuş bir Console uygulaması ve aşağıdaki kod parçası ile başlayalım.(System.Runtime.Serialization referansının eklenmiş olması gerektiğini unutmayın)

using System.IO;
using System.Runtime.Serialization;

namespace KnownTypes
{
    class Program
    {
        static void Main(string[] args)
        {
            // DataContractSerializer nesnesi örneklenirken parametre olarak serileştirilecek tip bilgisi verilir.
            XmlObjectSerializer serializer = new DataContractSerializer(typeof(Product));
            // Serileştirme işlemi yapılır. İlk parametre ile çıktının Product.xml isimli dosyaya yaplacağı belirtilir.
            // İkinci parametrede Product nesnesi örneklenir. Dikkat edilecek nokta Information özelliğine object tipinden bir nesne örneğinin aktarılmış olmasıdır.           
            serializer.WriteObject(
                new FileStream("Product.xml", FileMode.Create, FileAccess.Write)
                , new Product { Information = new object() }
                );

            // Object tipine string atama
            serializer.WriteObject(
                new FileStream("ProductV2.xml", FileMode.Create, FileAccess.Write)
                , new Product { Information = "Ürün hakkında çeşitli bilgiler" }
                );

            // Exception durumu
            //serializer.WriteObject(
            //    new FileStream("ProductV3.xml", FileMode.Create, FileAccess.Write)
            //    , new Product { Information = new ProductInformation { Id=1, Summary="Ürün için çeşitli bilgiler" } }
            //    ); 
        }
    }

    [DataContract]
    class Product
    {
        [DataMember]
        public object Information { get; set; }
    }

    // [DataContract]
    // class ProductInformation
    // {
    //     [DataMember]
    //     public string Summary { get; set; }
    //     [DataMember]
    //     public int Id { get; set; }
    // }
}


Örneğimizde Product isimli serileştirilebilir bir tip tanımlandığı görülmektedir. DataContract niteliği ile işaretlenmiş olan Product tipinin object tipinden Information isimli bir özelliği de bulunmaktadır ki işleri karıştıracak olan nokta burasıdır. Program kodu içerisindeki amaç, Product tipinden nesne örneklerinin nasıl serileştirildiğine bakmaktır. serializer isimli XmlObjectSerializer nesne örneği üzerinden yapılan ilk WriteObject çağrısındai Information özelliğine object tipinden bir değer atandığı görülmektedir. İkinci WriteObject çağrısında ise bir string değer atanmıştır ki bu son derece doğaldır. (Hatırlayalım .Net tipleri en üstte Object tipinden türemektedir. Bu nedenle her nesnenin Object tipi ile ifade edilebilmesi mümkündür.) Bu kod parçasını çalıştırdığımızda Product.xml ve ProductV2.xml isimli iki dosya üretildiğini görürüz. Önce Product.xml içeriğine ve şemasına bir bakalım.

Product.xml içeriği;

<Product xmlns="http://schemas.datacontract.org/2004/07/KnownTypes" xmlns:i="http://www.w3.org/2001/XMLSchema-instance"><Information/></Product>

Product.xsd içeriği;

<?xml version="1.0" encoding="windows-1254"?>
<xs:schema xmlns:i="http://www.w3.org/2001/XMLSchema-instance" attributeFormDefault="unqualified" elementFormDefault="qualified" targetNamespace="http://schemas.datacontract.org/2004/07/KnownTypes" xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:element name="Product">
    <xs:complexType>
      <xs:sequence>
        <xs:element name="Information" />
      </xs:sequence>
    </xs:complexType>
  </xs:element>
</xs:schema>

Tahmin ettiğimiz ve beklediğimiz gibi bir çıktı üretilmiştir. Ancak Information özelliğine string bir değer atanmasının sonucu oluşan çıktı biraz farklıdır. Bu noktada dikkatle duralım Sealed

ProductV2.xml içeriği;

<Product xmlns="http://schemas.datacontract.org/2004/07/KnownTypes" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
  <Information i:type="a:string" xmlns:a="http://www.w3.org/2001/XMLSchema">Ürün hakkında çeşitli bilgiler</Information>
</Product>

ProductV2.xsd içeriği;

<?xml version="1.0" encoding="utf-8"?>
<a:schema xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:a="http://www.w3.org/2001/XMLSchema" attributeFormDefault="unqualified" elementFormDefault="qualified" targetNamespace="http://schemas.datacontract.org/2004/07/KnownTypes">
  <xs:element name="Product">
    <xs:complexType>
      <xs:sequence>
        <xs:element name="Information" type="xs:string" />
      </xs:sequence>
    </xs:complexType>
  </xs:element>
</a:schema>

XSD şemasında, Information elementi için type niteliğinde xs:string kullanıldığı görülmektedir. Üstelik üretilen veri içeriğinde de i:type niteliğinde Information elementinin içeriğinin string veri tipinden olduğu işaret edilmektedir. Yani Information özelliğine atadığımız string değişken, primitive bir tip tanımana göre XML içerisinde bildirilmiştir. Zaten primitive tiplere dönüşüm yapıldığı takdirde pek bir sıkıntı yoktur. Söz gelimi Information özelliğine örneğin 12 değerini atadığımızda buna uygun olarak XML içeriğinde de int tipinin kullanıldığı görülebilir. Sorun yorum satırı kodlarını çalışır hale getirdiğimizde ortaya çıkmaktadır. Yeni ProductInformation tipini etkinleştirip aşağıdaki kodları çalıştırdığımızda... Undecided

// Exception durumu
            serializer.WriteObject(
                new FileStream("ProductV3.xml", FileMode.Create, FileAccess.Write)
                , new Product { Information = new ProductInformation { Id=1, Summary="Ürün için çeşitli bilgiler" } }
                );
               
Bu sefer Information özelliğine serileştirilebilir(ki DataContract ve DataMember nitelikleri nedeni ile) ProductInformation tipinden bir nesne örneği atanmaktadır. Bu durumda çalışma zamanında aşağıdaki ekran görüntüsünde yer alan SerializationException istisnasının alındığı görülür.

Bu istisna mesajından anlamamız gereken özlü söz şudur;

"Serileştirici tiplerin, serileştirecekleri nesne örneklerinin özelliklerinin tiplerinin neler olabileceğini açık bir şekilde bilmeye ihtiyaçları vardır. " Wink

Her ne kadar primitive bir tip kullanıldığında sorun olmasa da, yukarıdaki örnekte görüldüğü gibi bilinmeyen bir tipin atanmasında sorunlar yaşanabilir. Üstelik belkide atanan verinin tipinin, serileştirici tarafından varsayılan olarak atanan bir tip olmaması da gerekebilir. Söz gelimi Information özelliğine noktasız sayısal bir değer atandığında büyüklüğüne göre varsayılan olarak int tipi göz önüne alınacak ve XML içeriğinde bu yönde bir tanımlama olacaktır. Ki karşı tarafta belkide bu sayısal değerin string olarak değerlendirilmesi isteniyor olabilir! Upsss...

Peki serileştiricinin serileştirdiği tipin içeriğinde kullanılabilecek tipleri kesin olarak bilmesi nasıl sağlanabilir?

Yöntemlerden birisi ve belkide en basiti, aşağıdaki kod parçasında olduğu gibi serileştirilecek tip için KnownType niteliğini kullanmaktır.

Kişisel Not: Başka yöntemlerde bulunmaktadır. Örneğin tip içerisinde Type[] dizisi döndüren bir metodun adı KnownType niteliğinde kullanılarak birden fazla tipin bildirimi yapılabilir. Ya da servis sözleşmesinde ServiceKnownType niteliğinde bu bildirim yapılabilir. İşte size güzel bir araştırma konusu. Bu tekniklerin nasıl uygulanabileceğini araştırabilirsiniz. Özellikle ilk teknik dikkate değerdir. Yani bir metod aracılığıyla, KnowType niteliğine birden fazla tipin Type[] dizisi olarak bildirilmesi. Dikkat edilmesi gereken tek nokta Type[] dizisi döndüren metodun static olmasını sağlamaktır. Hatta buradan bir adım öteye gidip generic bir modelin KnownType niteliğine söz konusu metod yardımıyla aktarılması dahi sağlanabilir. Bu kadar ipucu yeter. Haydi klavye başına Wink

[DataContract]
[KnownType(typeof(ProductInformation))]
class Product
{
   [DataMember]
   public object Information { get; set; }
}

Bu şekildeki kullanım sonrasında artık SerializationException istisnası üretilmeyecek ve aşağıdaki çıktılar oluşacaktır.

ProductV3.xml içeriği;

<Product xmlns="http://schemas.datacontract.org/2004/07/KnownTypes" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
  <Information i:type="ProductInformation">
    <Id>1</Id>
    <Summary>Ürün için çeşitli bilgiler</Summary>
  </Information>
</Product>

Dikkat edileceği üzere ProductInformation nesne örneği içeriği ile birlikte Product elementi içerisine alınmıştır. Bu durumda şema içeriğinde gerekli bilgilendirmelerin aşağıdaki gibi yapıldığı görülecektir.

ProductV3.xsd içeriği

<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:i="http://www.w3.org/2001/XMLSchema-instance" attributeFormDefault="unqualified" elementFormDefault="qualified" targetNamespace="http://schemas.datacontract.org/2004/07/KnownTypes" xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:element name="Product">
    <xs:complexType>
      <xs:sequence>
        <xs:element name="Information">
          <xs:complexType>
            <xs:sequence>
              <xs:element name="Id" type="xs:unsignedByte" />
              <xs:element name="Summary" type="xs:string" />
            </xs:sequence>
          </xs:complexType>
        </xs:element>
      </xs:sequence>
    </xs:complexType>
  </xs:element>
</xs:schema>

Known Type sorunsalının bir sorunsal olarak değerlendirilmesinin ise iki sebebi vardır.

Birincisi, SOA düşünce tarzına aykırı olduğu görüşününün yaygın olmasıdır. Nitekim Shared Contract' ların söz konusu olduğu senaryolarda Interoperability sağlanırken, XML içerisindeki tipin karşı tarafça anlaşılabilir olması gerekmektedir. İkinci olarak serileştirme ve ters serileştirme işlemleri sırasında Known Type' a göre Reflection mekanizmasının devreye girmesi, XML üzerinde tip ile ilişkili bilgi edinme ve yazma gibi operasyonların söz konusu olmasından kaynaklanan performans kayıpları değerlendirilmektedir. Bu iki sebep nedeniyle zorunlu kalınmadıkça Known Type kullanımından kaçınılması önerilmektedir. Peki neden böyle bir konuya değindik? Aslında bir sonraki yazımıza zemin hazırlamaya çalışıyoruz. Nitekim WCF 4.0 tarafında bu konu ile ilişkili bir yenilik gelmesi muhtemeldir. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

KnownTypes.rar (22,64 kb)

Tags:  
Categories:   WCF
Actions:   E-mail | del.icio.us | Permalink | Yorumlar (0) | Comment RSSRSS comment feed
Bookmark and Share

Ado.Net Data Services 1.5 - Projections

Çarşamba, 30 Eylül 2009 09:00 by bsenyurt

Merhaba Arkadaşlar,

Gün geçmiyorki yazılım teknolojilerinde bir yenilik, bir güncelleme, bir genişletme çıkmasın...Özellikle dünyanın dev yazılım şirketlerinin en büyüğü olarak görebileceğimiz Microsoft tarafında bu gelişme ve güncelleme hızı oldukça yüksek. Gerçektende heyecan verici yenilikler, özellikler ile karşılaşmıyor değiliz. Bu konuya nereden mi geldim? Çok zaman değil daha bir sene öncesine kadar Astoria kod adlı Ado.Net Data Services konusunu incelemeye başlamıştım. Entity Framework veya Custom LINQ Provider' ları ile sunulan veri kümelerine, REST bazlı olarak URL sorgular atılabilmesini sağlayan ve özellikle Silverlight gibi RIA içeriklerinde son derece kıymetli olan bir servis uygulaması olarak değerlendirebileceğimiz bu konu ile ilişkili ilk paylaşımlarımı yaptıktan sonra araya WCF 4.0, WF 4.0, Design Patterns, Design Principles, .Net RIA Services gibi konular girdi. Bu konulardaki incelemelerimi ve paylaşımlarımı devam ettirirken bir baktım ki Ado.Net Data Services konusuna çok uzun zaman ara vermişim. Ara vermeklede iyi yapmamışım Undecided

Nitekim program yöneticisi olan Mike Flasko boş durmamış ve Ado.Net Data Services v1.5 versiyonu için CTP2 sürümünü duyurmuş(.Net Framework 3.5 Service Pack 1 ve Silverlight 3.0' ı hedefleyen ama .Net Framework 4.0 içerisinede dahil edilecek olan özellikleri içeren bir sürüm olarak düşünülebilir). Duyurulması ile birlikte hem blog sitesinde hemde çeşitli kaynaklarda konu ile ilişkili yazılar yayınlanmaya da başlanmış.

Bu versiyonda bazı yenilikler ve daha önceki sürüme ait çeşitli düzeltmeler(bug-fix) yer almakta. Gelen yeni özelliklerden birisi de Projections kullanımı. Bu yeniliğe göre servis üzerinde gerçekleştirilen URL bazlı sorguların sonuçları kırpılabiliyor ve sadece ilgilenilmek istenenlerin istemci tarafına çekilmesi sağlanabiliyor. Wink Bir başka deyişle, istemcinin yapmış olduğu bir talebin(Request) sonuçlarında sadece ilgilendiği özelliklerin getirilmesi sağlanabilmekte. Bunu tam olmasada, LINQ sorguları sırasında anonymous type kullanımına benzetebiliriz. Söz konusu özellik içerisinde primitive/complex tipleri veya navigation özelliklerini de kullanabilmekteyiz. Özelliğin getirisi, istemcinin talebi sonrası tüm Entity kümesinin işlenmesi ve ağ üzerinde hareket etmesi yerine, sadece istediği özellikleri içeren kümenin/kümelerin değerlendirilebilmesi olarak görülebilir. Bu çok doğal olarak istemci ile sunucu arasındaki trafiği boyutsal olarak azaltmaktadır. Projections kullanımı son derece basittir. Bunun için $select operatöründen yararlanılmaktadır. Tabiki konuyu anlamamızın en iyi yolu basit bir örneği adım adım geliştirmek ve üzerinde ilerlemekle olacaktır. Bu nedenle kolları sıvayıp işe koyulalım. İlk olarak Visual Studio 2008 ortamında(Service Pack 1 yüklü olan) basit bir Asp.Net Web Uygulaması oluşturarak işe başlayabiliriz. Sonrasında servisimiz için gerekli Entity kaynağını oluşturmamız gerekiyor. Bu amaçla Ado.Net Entity Framework' ten yararlanabilir ve yine kobay veritabanımız olan AdventureWorks' ü değerlendirebiliriz. Örneğimizde aşağıdaki EDM şemasını kullanıyor olacağız.

AdventureWorks veritabanındaki Production şemasında yer alan ProductCategory, ProductSubCategory ve Product tablolarını kullanmaya çalışıyoruz. Entity modelimizi oluşturduktan sonra, projemize yeni bir Ado.Net Data Services öğesi ekleyerek devam edebiliriz. Tabi bu seferki örneğimizde v1.5 CTP2 sürümüne ait öğeyi kullanmamız gerekiyor.

Ado.Net Data Services öğemizin kod içeriğini ise aşağıdaki gibi değiştirmemiz yeterli olacaktır.

using System.Data.Services;

namespace Projections
{
    public class AdventureServices
        : DataService<AdventureWorksEntities>
    {
        public static void InitializeService(DataServiceConfiguration config)
        {
            // Tüm Entity' leri sadece okuma amaçlı açıyoruz
            config.SetEntitySetAccessRule("*", EntitySetRights.AllRead);
            // İstemciden gelecek olan Projection taleplerinin değerlendirileceğini belirtiyoruz
            config.DataServiceBehavior.AcceptProjectionRequests = true;
            // Versiyon 2 için geliştirme yapacağımızı belirtiyoruz. Bu versiyon belirtilmediği takdirde select operatörü ve projection fonksiyonelliği çalışmayacaktır.
            config.DataServiceBehavior.MaxProtocolVersion = System.Data.Services.Common.DataServiceProtocolVersion.V2;
     }
    }
}

Dikkat edilmesi gereken noktalardan birisi AcceptProjectionRequest ise MaxProtocolVersion özelliklerine atanan değerlerdir. Bu değerlere göre servisimiz, istemcilere Projection fonksiyonelliğini sunabilecektir. AdventureServices.svc dosyasını bir tarayıcı yardımıyla talep ettiğimizde, başlangıç için aşağıdakine benzer bir ekran görüntüsü ile karşılaşırız.

Görüldüğü üzere Product, ProductCategory ve ProductSubcategory Entity' leri kullanılmaya hazırdır. Evetttt...Gelelim yazımızın önemli olan kısmına. Tarayıcı üzerinden aşağıdaki sorguyu talep ettiğimizi düşünelim.

http://localhost:1714/AdventureServices.svc/Product?$select=ProductID,Name,ListPrice

Dikkat edileceği üzere Product Entity' si üzerinden select sorgusu atılmış ve sadece ProductID,Name,ListPrice alanları talep edilmiştir. Bu sorgunun çalışma zamanı çıktısı aşağıdaki gibi olacaktır.

Dikkat edileceği üzere Product tablosundaki tüm ürünlerin sadece ProductID,Name ve ListPrice alanları çekilmiştir. İşin güzel yanı, bu URL talebi için arka planda çalıştırılan SQL sorgusuda sadece istenen alanları değerlendirmektedir. İşte URL' imize ait SQL sorgusunun SQL Server Profiler' dan yakalanan içeriği.

SELECT
1 AS [C1],
CASE WHEN ([Extent1].[ProductID] IS NULL) THEN N'' ELSE N'AdventureWorksModel.Product' END AS [C2],
N'ProductID,Name,ListPrice' AS [C3],
[Extent1].[ProductID] AS [ProductID],
[Extent1].[Name] AS [Name],
[Extent1].[ListPrice] AS [ListPrice]
FROM [Production].[Product] AS [Extent1]

Dolayısıyla Projection kullanılaraktan, bir Entity üzerinden sadece istenen alanları içeren çıktıların alınması sağlanabilir. Bu kullanım aynen SQL tarafı içinde geçerli olduğundan, performans adına da bazı kazanımların elde edildiği ortadadır.

select operatörünü dilersek navigasyon özellikleri(Navigation Properties) ilede bir aradada kullanabiliriz. Örneğin aşağıdaki gibi bir URL talebinde bulunduğumuzu düşünelim.

http://localhost:1714/AdventureServices.svc/ProductSubcategory?$select=Name,Product

Buna göre ProductSubcategory Entity' sinden sadece Name alanlarının değerlerini isterken, her alt kategoriye bağlı ürünleri tutan Product Entity örneklerini de talep etmekteyiz. Bu URL talebinin çıktısı aşağıdaki gibi olacaktır.

Dikkat edileceği üzere alt kategoriye bağlı olan Product kümeleri için sadece bağlantı bildirimi yapılmaktadır. Söz konusu URL' ın çalıştırılması sonucunda SQL tarafında da aşağıdaki sorgunun yürütüldüğü görülecektir.

SELECT
1 AS [C1],
CASE WHEN ([Extent1].[ProductSubcategoryID] IS NULL) THEN N'' ELSE N'AdventureWorksModel.ProductSubcategory' END AS [C2],
N'Name,ProductSubcategoryID' AS [C3],
[Extent1].[Name] AS [Name],
[Extent1].[ProductSubcategoryID] AS [ProductSubcategoryID]
FROM [Production].[ProductSubcategory] AS [Extent1]

Fark edilebileceği gibi, Product tablosu ile ilişkili bir sorgu ifadesi yer almamaktadır. Diğer yandan sadece Name alanını talep etmemize rağmen, PrimaryKey olan ProductSubcategoryID alanı da getirilmektedir. Bu son derece doğaldır nitekim, belirli bir ProductSubcategory' nin çekilmesinde primary key alanı ayırt edici özelliklerdendir üstelik entry/id elementleri içerisinde gereklidir. Diğer yandan, URL satırını aşağıdaki gibi değiştirirsek,

http://localhost:1714/AdventureServices.svc/ProductSubcategory?$select=Name,Product&$expand=Product&$top=2

Hımmm...Wink Bu sorguya göre ProductSubcategory içeriğinden sadece Name alanını almakla kalmıyor, aynı zamanda alt kategoriye bağlı olan ürünleride çekiyoruz. Üstelik sadece ilk 2 ProductSubcategory tipini ele alıyoruz(Sondaki top=2 sorgusu nedeniyle). İşte çalışma zamanı çıktımız.

Peki bu URL talebi sonrası arka planda nasıl bir SQL sorgusu çalışıyor?

SELECT
[Project2].[ProductSubcategoryID] AS [ProductSubcategoryID],[Project2].[Name] AS [Name], [Project2].[rowguid] AS [rowguid], [Project2].[ModifiedDate] AS [ModifiedDate], [Project2].[C1] AS [C1], [Project2].[C2] AS [C2], [Project2].[C3] AS [C3], [Project2].[C4] AS [C4], [Project2].[C5] AS [C5], [Project2].[C6] AS [C6], [Project2].[C7] AS [C7], [Project2].[ProductID] AS [ProductID], [Project2].[Name1] AS [Name1], [Project2].[ProductNumber] AS [ProductNumber], [Project2].[MakeFlag] AS [MakeFlag], [Project2].[FinishedGoodsFlag] AS [FinishedGoodsFlag], [Project2].[Color] AS [Color], [Project2].[SafetyStockLevel] AS [SafetyStockLevel], [Project2].[ReorderPoint] AS [ReorderPoint], [Project2].[StandardCost] AS [StandardCost], [Project2].[ListPrice] AS [ListPrice], [Project2].[Size] AS [Size], [Project2].[SizeUnitMeasureCode] AS [SizeUnitMeasureCode], [Project2].[WeightUnitMeasureCode] AS [WeightUnitMeasureCode], [Project2].[Weight] AS [Weight], [Project2].[DaysToManufacture] AS [DaysToManufacture], [Project2].[ProductLine] AS [ProductLine], [Project2].[Class] AS [Class], [Project2].[Style] AS [Style], [Project2].[ProductModelID] AS [ProductModelID], [Project2].[SellStartDate] AS [SellStartDate], [Project2].[SellEndDate] AS [SellEndDate], [Project2].[DiscontinuedDate] AS [DiscontinuedDate], [Project2].[rowguid1] AS [rowguid1], [Project2].[ModifiedDate1] AS [ModifiedDate1]
FROM ( SELECT
 [Limit1].[ProductSubcategoryID] AS [ProductSubcategoryID],  [Limit1].[Name] AS [Name],  [Limit1].[rowguid] AS [rowguid],  [Limit1].[ModifiedDate] AS [ModifiedDate],  [Limit1].[C1] AS [C1],  [Limit1].[C2] AS [C2],  [Limit1].[C3] AS [C3],  [Limit1].[C4] AS [C4],  [Limit1].[C5] AS [C5],  [Limit1].[C6] AS [C6],  [Extent2].[ProductID] AS [ProductID],  [Extent2].[Name] AS [Name1],  [Extent2].[ProductNumber] AS [ProductNumber],  [Extent2].[MakeFlag] AS [MakeFlag],  [Extent2].[FinishedGoodsFlag] AS [FinishedGoodsFlag],  [Extent2].[Color] AS [Color],  [Extent2].[SafetyStockLevel] AS [SafetyStockLevel],  [Extent2].[ReorderPoint] AS [ReorderPoint],  [Extent2].[StandardCost] AS [StandardCost],  [Extent2].[ListPrice] AS [ListPrice],  [Extent2].[Size] AS [Size],  [Extent2].[SizeUnitMeasureCode] AS [SizeUnitMeasureCode],  [Extent2].[WeightUnitMeasureCode] AS [WeightUnitMeasureCode],  [Extent2].[Weight] AS [Weight],  [Extent2].[DaysToManufacture] AS [DaysToManufacture],  [Extent2].[ProductLine] AS [ProductLine],  [Extent2].[Class] AS [Class],  [Extent2].[Style] AS [Style],  [Extent2].[ProductModelID] AS [ProductModelID],  [Extent2].[SellStartDate] AS [SellStartDate],  [Extent2].[SellEndDate] AS [SellEndDate],  [Extent2].[DiscontinuedDate] AS [DiscontinuedDate],  [Extent2].[rowguid] AS [rowguid1],  [Extent2].[ModifiedDate] AS [ModifiedDate1], 
 CASE WHEN ([Extent2].[ProductID] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C7]
 FROM   (SELECT TOP (2) [Project1].[ProductSubcategoryID] AS [ProductSubcategoryID], [Project1].[Name] AS [Name], [Project1].[rowguid] AS [rowguid], [Project1].[ModifiedDate] AS [ModifiedDate], [Project1].[C1] AS [C1], [Project1].[C2] AS [C2], [Project1].[C3] AS [C3], [Project1].[C4] AS [C4], [Project1].[C5] AS [C5], [Project1].[C6] AS [C6]
  FROM ( SELECT
   [Extent1].[ProductSubcategoryID] AS [ProductSubcategoryID],
   [Extent1].[Name] AS [Name],
   [Extent1].[rowguid] AS [rowguid],
   [Extent1].[ModifiedDate] AS [ModifiedDate],
   1 AS [C1],
   1 AS [C2],
   CASE WHEN ([Extent1].[ProductSubcategoryID] IS NULL) THEN N'' ELSE N'AdventureWorksModel.ProductSubcategory' END AS [C3],
   N'Name,ProductSubcategoryID' AS [C4],
   N'Product' AS [C5],
   1 AS [C6]
   FROM [Production].[ProductSubcategory] AS [Extent1]
  )  AS [Project1]
  ORDER BY [Project1].[ProductSubcategoryID] ASC ) AS [Limit1]
 LEFT OUTER JOIN [Production].[Product] AS [Extent2] ON [Limit1].[ProductSubcategoryID] = [Extent2].[ProductSubcategoryID]
)  AS [Project2]
ORDER BY [Project2].[ProductSubcategoryID] ASC, [Project2].[C7] ASC

Amanınnnn!!! Sealed Aslında biraz can sıkıcı ama doğal olarak tüm Product alanlarının değerlendirildiğini görüyoruz. Nitekim aksini belirtmedik. Peki belirtebilir miyiz? Yani ProductSubcategory kümesinden ve genişletilebilen Product kümesinden bir kaç alanı almayı başarabilir miydik? İşte örnek bir cevabı Cool

http://localhost:1714/AdventureServices.svc/ProductSubcategory?$select=Name,Product/Name,Product/ListPrice&$expand=Product&$top=5

Görüldüğü gibi EntityAdı/AlanAdı(örneğin Product/Name) stilinde yapılan bildirimlerle, üretilecek olan çıktıda birden fazla Entity' den gelebilecek alanları ayrı ayrı belirtebiliyoruz. (Buna göre sizlerde ProductCategory,ProductSubcategory ve Product kümelerinin tamamının bir arada bulunduğu örnek URL üzerinde çalışabilirsiniz. Çalışmanızı öneririm.)

Görüldüğü üzere alt kategori ile ilişkili Feed girişlerinde Name alanı yer almaktayken, o alt kategoriye bağlı Product tipleri için sadece Name ve ListPrice değerleri getirilmektedir. Dolayısıyla SQL sorgusuda buna göre aşağıda görüldüğü gibi oluşacaktır.

SELECT
[Project2].[ProductSubcategoryID] AS [ProductSubcategoryID], [Project2].[Name] AS [Name], [Project2].[C1] AS [C1], [Project2].[C2] AS [C2], [Project2].[C3] AS [C3], [Project2].[C4] AS [C4], [Project2].[C5] AS [C5],
[Project2].[C9] AS [C6], [Project2].[C6] AS [C7], [Project2].[C7] AS [C8], [Project2].[C8] AS [C9], [Project2].[Name1] AS [Name1], [Project2].[ListPrice] AS [ListPrice], [Project2].[ProductID] AS [ProductID]
FROM ( SELECT
 [Limit1].[ProductSubcategoryID] AS [ProductSubcategoryID],  [Limit1].[Name] AS [Name],  [Limit1].[C1] AS [C1],  [Limit1].[C2] AS [C2],  [Limit1].[C3] AS [C3],  [Limit1].[C4] AS [C4],  [Limit1].[C5] AS [C5],  [Extent2].[ProductID] AS [ProductID],  [Extent2].[Name] AS [Name1],  [Extent2].[ListPrice] AS [ListPrice],  CASE WHEN ([Extent2].[ProductID] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C6],
 CASE WHEN ([Extent2].[ProductID] IS NULL) THEN CAST(NULL AS varchar(1)) ELSE CASE WHEN ([Extent2].[ProductID] IS NULL) THEN N'' ELSE N'AdventureWorksModel.Product' END END AS [C7],
 CASE WHEN ([Extent2].[ProductID] IS NULL) THEN CAST(NULL AS varchar(1)) ELSE N'Name,ListPrice,ProductID' END AS [C8],
 CASE WHEN ([Extent2].[ProductID] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C9]
 FROM   (SELECT TOP (5) [Project1].[ProductSubcategoryID] AS [ProductSubcategoryID], [Project1].[Name] AS [Name], [Project1].[C1] AS [C1], [Project1].[C2] AS [C2], [Project1].[C3] AS [C3], [Project1].[C4] AS [C4], [Project1].[C5] AS [C5]
  FROM ( SELECT
   [Extent1].[ProductSubcategoryID] AS [ProductSubcategoryID], [Extent1].[Name] AS [Name], 1 AS [C1], 1 AS [C2],
   CASE WHEN ([Extent1].[ProductSubcategoryID] IS NULL) THEN N'' ELSE N'AdventureWorksModel.ProductSubcategory' END AS [C3],
   N'Name,ProductSubcategoryID' AS [C4],
   N'Product' AS [C5]
   FROM [Production].[ProductSubcategory] AS [Extent1]
  )  AS [Project1]
  ORDER BY [Project1].[ProductSubcategoryID] ASC ) AS [Limit1]
 LEFT OUTER JOIN [Production].[Product] AS [Extent2] ON [Limit1].[ProductSubcategoryID] = [Extent2].[ProductSubcategoryID]
)  AS [Project2]
ORDER BY [Project2].[ProductSubcategoryID] ASC, [Project2].[C9] ASC

Görüldüğü üzere Ado.Net Data Services v1.5 CTP2 ile gelen Projection özelliği performans kazanımı elde etmemizi sağlayacak derecede önemli bir özellik olarak karşımıza çıkmaktadır. Bu yazımızda kullandığımız sorgular aşağıdaki gibidir.

  • http://localhost:1714/AdventureServices.svc/Product?$select=ProductID,Name,ListPrice ->(Product kümesinden ProductID, Name ve ListPrice alanları alınır)
  • http://localhost:1714/AdventureServices.svc/ProductSubcategory?$select=Name,Product -> (ProductSubcategory kümesinden Name alınır, her bir alt kategoriye bağlı Product kümelerinin sadece linkleri getirilir.)
  • http://localhost:1714/AdventureServices.svc/ProductSubcategory?$select=Name,Product&$expand=Product&$top=2 -> (Bir önceki sorgu değerlendirilir ama Product kümesinin tüm üyeleri ve sadece ilk iki alt kategori tipi çekilir)
  • http://localhost:1714/AdventureServices.svc/ProductSubcategory?$select=Name,Product/Name,Product/ListPrice&$expand=Product&$top=5 -> (Bir önceki sorgu çalışır ancak Product kümesinden sadece Name ve ListPrice alanları hesaba katılır. Alt kategorilerinde ilk 10 adedi getirilir.)

Bakalım Ado.Net Data Services 1.5 CTP2 tarafında bizleri başka ne gibi sürprizler beklemekte. Bu konularıda ilerleyen yazılarımızda değerlendirmeye çalışıyor olacağız. Tekrardan görüşünceye dek hepinize mutlu günler dilerim .

Projections.rar (53,71 kb)

WCF Rest Starter Kit Preview 2 ile Twitter Reader

Çarşamba, 5 Ağustos 2009 08:47 by bsenyurt

Merhaba Arkadaşlar,

Minik bir çocukken Televizyon bağımlılığı(Malesef bu aptal kutuda çok vakiy geçirebiliyor çocuklar Frown ) nedeniyle pek çok çizgi filmi izlediğimi hatırlıyorum. Voltran, Transformers, Red Kit ve Daltonlar, Denver The Last Dinasour, Clementine filan derken arada sıradada "Bi kedi gördüm sanki" diyen Tweety Laughing Şimdi bu konuya nereden geldiğimi düşünebilirsiniz. Şu sıralarda sık sık takip ettiğim geekswithblogs sitesinde twitter' da(Sanırım niye cikcik diyerek yazımıza başladığımızı anlamışsınızdır Laughing )

yayınlanan içeriklerin WCF Rest Starter Kit Preview 2 ile birlikte gelen HttpClient sınıfı yardımıyla nasıl kolayca ele alınabileceğine dair bazı yazılar gördüm. Konunun içerisinde REST bazlı iletişim ve WCF söz konusu olunca hemen kolları sıvadım ve Windows tabanlı basit bir örnek geliştirmeye karar verdim. Tabi başlamadan önce projemizin amacından biraz bahsetmek isterim. Twitter üzerinde yayınlanan girişleri HTTP üzerinden GET metodu ile çekmeyi, buna göre eklenen güncel içerikleri uygulamamızda göstermeyi ve yenilerinide kendi Twitter hesabımız üzerinden, HTTP Post metodu ile ekleyebilmeyi planlıyoruz. Aslında olay bir RSS Reader yazmak kadar basit. Diğer yandan burada bahsettiğimiz işlevsellikleri geliştirmek için elimizde WCF Rest Starter Kit Preview 2 olmasına da gerek yoktur. Ancak Kit' in bize sağladığı bazı avantajlar ve kolaylıklar bulunmaktadır. Örneğin, XML içeriğini managed tarafta kolayca ele alabilmemiz için gerekli tiplerin üretimini kolaylaştıran Paste XML As Types Laughing Örneği geliştirebilmek için çok sık kullanmasamda Twitter' da bir hesap oluşturdum ve bildiğim geliştiricilerin Tweet' lerini takip etmeye başladım. İşe başlamadan önce, Twitter'da ne olup bittiğine bir bakayım dedim.

İşte buradaki içeriği Windows Uygulamasına çekmeyi hedefliyoruz. Rest Starter Kit' in nimetlerinden yararlanabilmek için, Windows uygulamasını oluşturulduktan sonra, WCF Rest Starter Kit Preview 2 ile birlikte gelen Microsoft.Http ve Microsoft.Http.Extensions assembly' larının projeye referans edilmesi gerekmektedir.

Referansların eklenmesinden sonra yolumuza, Twitter' da yayınlanan Feed içeriğinin managed taraftaki karşılıklarını oluşturarak devam edebiliriz. Bu noktada, http://twitter.com/statuses/friends_timeline.xml adresinde keni twitter hesabım ile baktığımda aşağıdaki ekran görüntüsünde yer alan XML içeriği ile karşılaştığımı gördüm.

Bu içeriği sayfanın View Source kısmını kullanarak kopyalayıp, Paste XML as Types seçeneğini yardımıyla(WCF Rest Starter Kit Preview 2 ile Visual Studio 2008 ortamına eklenmiştir) yönetimli kod tarafına dönüştürebiliriz. (Burada XML içeriğinden managed tipleri oluştururken dikkat edilmesi gereken noktalardan birisi, https değil http ile talepte bulunmamızdır. Aksi durumda View Source seçeneği çalışmamaktadır.) Diğer taraftan bu adres talebi sonrası var olan twitter hesabımız ile giriş yapmamız gerekmektedir. İşlem başarılı bir şekilde tamamlandığında sınıf diagramında aşağıdaki tiplerin oluştuğunu gözlemledim.

Harika! Görüldüğü üzere XML içeriğinin karşılığı olan sınıflar başarılı bir şekilde oluşturulmuştur. Dikkat edilmesi gereken noktalardan birisi, XML deki Child Node bağlantılarının sınıf bazında nasıl ifade edildiğidir. Örneğin statuses sınıfı içerisinde statusesStatus tipinden bir dizi olarak tanımlanmış status özelliği... Bu özelliği kod tarafında değerlendirerek, tweet içeriğini giren User' a dahi ulaşabiliriz. Nitekim bunun için statusesStatus sınıfı içerisinde, statusesStatusUser tipinden user isimli bir özellik tanımlanmıştır. Zaten işin en önemli kısımlarından biriside bu XML içeriğinin, kod tarafınaki ifade şekli değil midir? Teşekkürler Paste Xml As Types Laughing Managed tiplerde oluşturulduğuna göre artık arka plan kodlarımızı geliştirebiliriz. (Hemen şunu hatırlatalım.Paste Xml As Types ile üretilen sınıf ve üyelerinin adlarını dilediğiniz gibi değiştirebilirsiniz. Özellikle yazım standartlarına uygun-CamelCasing isimlendirmeler yapılmasında yarar vardır. Şu an örneği hızlı bir şekilde geliştirme istediğinde bu noktaları atlamış bulunuyorum) Windows Form' unu aşağıdaki gibi tasarlayabiliriz.

ve kodlarımız;

using System;
using System.Net;
using System.Windows.Forms;
using System.Xml.Serialization;  // ReadAsXmlSerializable<> genişletme metodunun çıkması için eklenmelidir.
using Microsoft.Http;// HttpClient için gerekli olan isim alanı.
using System.Drawing;

namespace TwitterReader
{
    public partial class Form1 : Form
    {
        // Siz kendi Twiteer kullanıcı adı ve şifrenizi kullanmalısınız.
        private string username = "sizin kullanıcı adınız";
        private string password = "sizin şifreniz";

        public Form1()
        {
            InitializeComponent();
        }

        private void btnGetFeeds_Click(object sender, EventArgs e)
        {
            try
            {
                pnlFeeds.Controls.Clear();

                // Öncelikle HttpClient nesne örneği oluşturulur.
                HttpClient client = new HttpClient();
                // Xml verisini yukarıdaki adresten çekebilmek için geçerli bir Twitter hesabı ile erişmemiz gerekecektir. Bu nedenle NetworkCredential oluşturulur ve Credentials koleksiyonuna eklenir.
                client.TransportSettings.Credentials = new NetworkCredential(username, password);
                ServicePointManager.Expect100Continue = false;

                // XML içeriğine, HttpClient nesne örneği üzerinden GET talebinde bulunulur. Sonuç HttpResponseMessage nesne örneğine aktarılır.
                HttpResponseMessage response = client.Get("http://twitter.com/statuses/friends_timeline.xml");
                // İşlemin başarılı olunduğundan emin olunması sağlanır. HTTP 200 OK Kontrolu
                response.EnsureStatusIsSuccessful();

                // XML İçeriği okunur ve statuses tipinden nesne örneğine aktarılır.
                statuses stats = response.Content.ReadAsXmlSerializable<statuses>();

                // statuses nesne örneğinin status özelliği ile işaret edilen statusesStatus tipinden dizinin her bir elemanı dolaşılır.
                foreach (statusesStatus s in stats.status)
                {
                    // Örnek içeriği göstermek amacıyla her bir statusesStatus için birer Label üretilir

                    Label lbl = new Label();
                    lbl.AutoSize = false;
                    lbl.Width = pnlFeeds.Width - 25;
                    lbl.BorderStyle = BorderStyle.FixedSingle;
                    lbl.Height = 75;
                    // Örnek bilgi olarak bilginin ne zaman eklendiği, içeriği ve kim tarafından oluşturulduğu ele alınır
                    lbl.Text = string.Format("{0} \n {1} ({2})", s.created_at, s.text, s.user.name);

                    pnlFeeds.Controls.Add(lbl);
                }
            }
            catch
            {
                lblStatus.Text = "Error!";
            }
        }

        private void btnPostNew_Click(object sender, EventArgs e)
        {
            try
            {
                // Öncelikle HttpClient nesne örneği oluşturulur. Parametre olarak takip edeceğimiz twitter adresi girilir.
                HttpClient client = new HttpClient("http://twitter.com/statuses/");
                // Xml verisini yukarıdaki adresten çekebilmek için geçerli bir Twitter hesabı ile erişmemiz gerekecektir. Bu nedenle NetworkCredential oluşturulur ve Credentials koleksiyonuna eklenir.
                client.TransportSettings.Credentials = new NetworkCredential(username, password);
                ServicePointManager.Expect100Continue = false;

                HttpResponseMessage response = client.Get("friends_timeline.xml");

                // Yeni bilgi girişi için bir form oluşturulur
                HttpUrlEncodedForm form = new HttpUrlEncodedForm();
                // status için TextBox1 kontrolünün içeriğinin girileceği belirtilir
                form.Add("status", txtEntry.Text);
                // Bu kez HTTP Post metoduna göre update.xml adresine form içeriği gönderilir
                response = client.Post("update.xml", form.CreateHttpContent());
                // İşlemin başarılı olduğundan emin olunur
                response.EnsureStatusIsSuccessful();

                lblStatus.Text = "Entry posted.";
            }
            catch
            {
                lblStatus.ForeColor = Color.Red;
                lblStatus.Text = "Aaa...Houston...We have a problem...";
            }
        }
    }
}

Örneğimizi çalıştırıp Get Feed başlıklı Button kontrolüne tıkladığımda aşağıdakine benzer sonuçlar ile karşılaştım.

Görüldüğü üzere örneği geliştirdiğim sıradaki tüm Tweet girişlerini elde edebilmiştim. Evet, tasarım biraz kötü Embarassed Hatta çok kötü Laughing Dahada güzelleştirilmesini size bırakıyorum.

Peki yeni bir Tweet girdiğimizde. Örneğin aşağıdaki ekran görüntüsündeki gibi,

İşlem başarılı olduktan sonra Twitter sitesine baktığımda aşağıdaki gibi yeni Tweet' in eklenmiş olduğunu gördüm. Bu arada tweet' in kaynağı olarak from API yazdığına dikkat edin. Bu dış ortama sunulan Twitter API yardımıyla bir giriş yaptığımızı ifade etmektedir. Peki biz bu API için projemize bir referans ekledik mi? Hayır. Nitekim API'yi REST bazlı olarak kullanıyoruz. İşte işin güzel yanlarından birisi daha.

Windows uygulamasında tekrardan Get Feed düğmesini kullandığımda aşağıdaki gibi son eklenen Tweet bilgisinin de geldiğini gördüm.

Aslında web üzerinden takip edilen Tweet içeriğinin bir Windows uygulaması yerine zengin görselliğe sahip bir WPF uygulamasında ele alınması çok daha şık sonuçlar doğurabilir. Bu örnekte bizim için dikkate değer olan noktalardan biriside, WCF Rest Starter Kit Preview 2 ile birlikte gelen HttpClient, Paste Xml As Types gibi yeniliklerin, gerçek hayat senaryosunda başarılı bir şekilde ele alınabilmiş olmasıdır.

Peki örnekte yapmadıklarım neler?

Her şeyden önce asenkron bir erişim söz konusu değildir. Bu nedenle verilerin çekilmesi veya yeni bir Tweet' in eklenmesi sırasında ekranda donmalar olmaktadır. Diğer taraftan çok güçlü bir Exception yönetimiz yok. Belki bir loglama sistemi koyarark Exception' ların uygulamayı geliştirenler için saklanması sağlanabilir. Örneği geliştirirken bir Exception almamış olmama rağmen Tweet bilgilerinin çekilmesi sırasında bağlantıdaki aksaklıklar nedeni ile Time out istisnalarına düşülebileceğini tahmin ediyorum. Bunu kod içerisinde kontrollü bir şekilde ele almak yerinde olacaktır. Diğer taraftan Button kontrolü yardımıyla veri çekmek yerine, kullanıcının kendisinin set edebileceği zaman dilimleri içerisinde veri çekilmesi sağlanabilir. Bu işi bir Timer bileşeni kolayca halledebilir. Birde tasarım konusunda beni takip etmemenizi öneririm Wink Bunları deneyin ve çok daha iyisini yapmaya çalışın. Umarım yararlı bir yazı olmuştur. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

TwitterReader.rar (187,66 kb)

Tags:  
Categories:   WCF
Actions:   E-mail | del.icio.us | Permalink | Yorumlar (3) | Comment RSSRSS comment feed
Bookmark and Share

REST Starter Kit Nedir?

Salı, 5 Mayıs 2009 21:41 by bsenyurt

Merhaba Arkadaşlar,

Bildiğiniz üzere bir süredir WCF REST Starter Kit ile ilişkili yazılarımı ve görsel derslerimi sizlerle paylaşmaktayım. Ancak önemli bir noktayı kaçırdığımı düşünüyorum. Embarassed Nedir bu WCF REST Starter Kit? Bizlere ne gibi avantajlar getirmektedir?

WCF REST Starter Kit temel olarak, WCF servislerinin REST(REpresentational State Transfer) bazlı olaraktan geliştirilmesi için gerekli özellik ve şablonları(Visual Studio Templates) içermekte olan bir yardımcı araç kitidir. Bu kit .Net Framework 3.5 ve SP1 ile birlikte, WCF tarafına kazandırılan Web programlama modelini kullanır ve geliştiricinin, REST bazlı WCF servislerinin yazılması ve tüketilmesi sırasında gerekli olan bir çok kıstasın(aspect olarakta düşünebiliriz) kolayca ele alabilmesini hedefler. WCF Rest Starter Kit, konsept olarak WCF servislerini ve REST modelini hedef almakla birlikte özellikle açık kaynak kodlu olması açısındanda önemli bir eklenti olarak düşünülmelidir.

NOT : Aslında Xml Web Servisleri içinde zamanında Web Service Enhancements adıyla yayımlanmış bir yardımcı kit bulunmaktadır. WSE' nin en büyük amacı, Xml Web Servislerinde çok fazla kodlamayı gerektiren bazı standart kıstasların(Transaction Flow, Security, MTOM bazlı dosya aktarımı vb...) nitelik veya konfigurasyon bazında daha kolay bir şekilde uygulanabilmesidir.

Kit aslında iki ana bölümden oluşmaktadır.

1- Sunucu Tarafı Yetenekleri; REST tabanlı WCF servislerinin host edildiği sunucu tarafı ile ilgili özelliklerdir. Örneğin,

  • dekleratif olarak ön bellekleme özelliklerini tanımlayabilmek(Declarative Caching),
  • hata yönetimi(Error Handling),
  • güvenlik(Security),
  • yardım desteği(Help Page Support),
  • büyük boyutlu dosyaların sunucu kaynaklarını fazla yormada taşınması(push style streaming)...

Bunlara ek olarak Visual Studio 2008 ortamına eklenen pek çok şablonda yer almaktadır.

  • Koleksiyon bazlı servisler(Rest Collection Wcf Service),
  • tek kaynaklı servisler(Rest Singleton Wcf Service),
  • Atom protokolü bazlı servisler(Atom Publishing Protocol WCF Service),
  • Atom bazlı içerik yayınlama servisi(Atom Feed WCF Service),
  •  ve basit olarak sadece HTTP bazlı taleplere cevap verecek olan WCF Servisleri(HTTP Plain XML WCF Service)

2- İstemci Tarafı Yetenekleri; İstemcilerin, REST tabanlı WCF servislerini kolayca kullanabilmeleri için gerekli özelliklerdir. REST Starter Kit’in ikinci versiyonunda, istemci tarafından REST mesajlarının gönderilmesi ve cevapların işlenmesini kolaylaştıracak şekilde HttpClient isimli yeni bir sınıf geliştirilmiştir. Ayrıca Visual Studio IDE’ sine Paste Xml As Type isimli bir diğer özellik katılarak(add-in), XML içeriklerinin(XSD şemasıda kullanılabilir) serileştirilebilir tip haline dönüştürülmesi de son derece kolaylaştırılmaktadır. Bu sayede servis tarafından yayımlanan bir XML içeriğin copy-paste tekniğini kullanarak ve Paste Xml As Type seçeneğinden yararlanarak, managed tarafta ele alınabilecek bir tip haline getirebiliriz.

Bu iki ayrım haricinde, Starter Kit ile birlikte gelip göze çarpan bir kaç noktayıda, aşağıdaki gibi özetleyebiliriz.

Çıktı formatları için destek(Representation Format) : Günümüz web uygulamalarında, istemci tarafının ele aldığı en popular içerik formatları, XML(Xtensible Markup Language) ve JSON(JavaScript Object Notation) dır. REST bazlı WCF servisleri zaten bu tipte yayınlama yapma yeteneğine sahiptir. Starter Kit’ in burada kattığı diğer bir yetenek ise, istemciden gelen HTTP-GET talebine bakarak geriye XML veya JSON formatında içerik gönderilmesini kolaylaştırmaktır.

Dekleratif Ön bellekleme(Declerative Caching) : Deklerafit kelimesinin burada kattığı anlam aslında, söz konusu özelliğin bir aspect olarak ele alınabilmesidir. Buna gore nitelik(attribute) ve konfigurasyon(web.config örneğin) bazında önbellekleme ile ilişkili bildirimler kolayca yapılabilir. Starter Kit burada işlemleri kolaylaştırmak için WebCache niteliğini sunmaktadır.

Yardım Desteği(Help Support) : REST bazlı WCF servislerinin istemci tarafından nasıl kullanılabileceğinin bilinmesi önemlidir. Burada servise giden taleplerin URI formatında olduğu düşünüldüğünde, servisin sunduğu operasyonların URI taleplerinin ne olacağı, operasyonun özet olarak ne yaptığı gibi bilgileri istemciye sunmak için REST starter kit /help sayfa desteğini getirmektedir. Burada, operasyonlar açısından önem arz eden konulardan biriside WebHelp niteliği yardımıyla özet bilgilerin belirtilmesi ve yardım sayfasında gösterilmelerinin sağlanmasıdır.

Hata Desteği(Error Handling) : Servis tarafında bir hata oluştuğunda bunun istemci tarafında string veya geliştirici tarafından yazılmış bir .Net tipi olarak XML veya JSON formatında döndürülebilmesi, starter kit ile dahada kolaylaştırılmaktadır. Burada WebProtocolException tipinden yararlanılmaktadır. Tabi JSON veya XML tipinden dönüş olacağına WebGet veya WebInvoke niteliklerindeki, ResponseFormat özelliğine atanan değer ile karar verilir.

Service Host: REST Starter kit, daha az konfigurasyon ile çalışma zamanının pek çok değerinin ayarlanabilmesini sağlamaktadır. Bu nedenle REST Starter Kit, WebServiceHost2 isimli bir tip içermektedir. Bu tipi, Visual Studio 2008 altındaki REST şablonlarını oluşturduğumuzda, svc öğelerine ait markup dosyaları içerisinde görebiliriz.

Güvenlik (Security) : REST bazlı WCF servislerini, starter kit ile geliştirirken, Asp.Net güvenlik özelliklerden yararlanılabilir. Böylece, örneğin form tabanlı doğrulama(Form Based Authentication) veya rol bazlı yetkilendirme(Role Based Authorization) yapılabilir. Bu işlemler için Asp.Net' in standart Membership API' sinden yararlanılabilir yada özel provider' lar kullanılabilir.

HttpClient : İstemci tarafına getirilen bu tip ile, REST bazlı WCF servislerini HTTP Put, Get, Delete, Post metodlarına göre kullanmak dahada kolaylaşmaktadır. Ayrıca asenkron programlama(async programming) modeline de destek verilmektedir. Üstelik, olay bazlı asenkron programlama(Event Based Asnyc Programming) modelide kullanılabilmektedir. Ayrıca Atom protokolüne gore yayın yapan bir servisin istemci tarafından ele alınması için AtomPubClient sınıfıda HttpClient tipini kullanmak üzere, REST Starter Kit’ e getirilmiş genişletmelerden birisidir.

Tabiki daha pek çok detay bulunmaktadır. Ancak sanıyorumki bu kısa bilgiler sizlerde WCF Rest Starter Kit hakkında bir fikir oluşturmuştur. 

Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Tags:   ,
Categories:   WCF
Actions:   E-mail | del.icio.us | Permalink | Yorumlar (0) | Comment RSSRSS comment feed
Bookmark and Share

REST Bazlı WCF Servislerinde AdapterStream Kullanımı

Perşembe, 30 Nisan 2009 01:03 by bsenyurt

Merhaba Arkadaşlar,

REST bazlı WCF servislerinde zaman zaman istemcilere içerik boyutları yüksek olan çıktılar veriyor olabiliriz. Bunlara örnek olarak resim veya metin dosyaları verilebilir. Aslında Stream veya TextWriter bazlı içerikler dersek çok daha doğru olacaktır.(Neden TextWriter olarak belirttiğimi yazının sonunda öğrenebileceğiz.) Özellikle istemci/sunucu bazlı uygulamalar göz önüne alındığında, büyük boyutlu içeriklerin karşı tarafa aktarılması sırasında karşılaşılabilecek pek çok performans kaybı söz konusudur. Sunucu tarafından bakıldığında, istemcinin talep ettiği içeriğin Stream olarak elde edilmesi sırasında bellek ve işlemci bazında yüklenmeler olabilir. Buda sunucunun performansının olumsuz yönde etkiliyebilir. Nitekim kaynakların israfı söz konusudur. Tabi istemci tarafı açısından bakıldığında da, gelen Stream içeriğinin işlenmesi esnasında bazı sıkıntılar ile karşılaşılabilir.

Biz bu yazımızda sunucu tarafındaki içeriğin Stream olarak elde edilmesi sırasında performans kazanımı için ne yapabileceğimize bakacağız. Neyse ki çok fazla uğraşmamıza gerek yok. Nitekim WCF Rest Starter Kit ile birlikte gelen AdapterStream sınıfı tam bu iş için geliştirilmiş bir tip. Üstelik kit ile birlikte gelen örnek solution içerisinde kaynak kodunu görmenizde mümkün. Bu sınıf yardımıyla bir Stream' in hazırlanması sırasında, içeriğin tamamıyla değil, parça parça aktarılması sağlanabiliyor. Buda bir anlamda sunucunun bellek ve işlemci kaynaklarının daha az yorulması anlamına gelmekte.

Aslında hiç vakit kaybetmeden bu konu ile ilişkili geliştirdiğim örneği sizinle paylaşmak istiyorum. Öncesinde nacizane senaryomdan biraz bahsedeyim. Servis tarafında yer alan basit bir operasyon, talep ile kendisine gelen kelimeye bakarak, istemci için bir duvar kağıdı resminin üretilmesini sağlamakta. Burada gelen kelimeleri çok basit bir düşünce ile bazı resim dosyaları ile eşleştirmekteyim. Eşleştirme için basit bir XML dökümanı kullanıyorum. Böylece servis tarafına yeni resimler ve bu resimlere eş düşecek kelimelerin koda müdahale etmeye gerek kalmadan eklenmesi mümkün olabilir. Kabaca aşağıdaki gibi bir durumdan bahsediyorum aslında.

Servis projesinin klasör yapısı,

ve Mapper.xml içeriği,

<?xml version="1.0" encoding="utf-8" ?>
<Mapper>
  <Map Keyword="ay" Image="images/Ay.jpg"/>
  <Map Keyword="bizimsokak" Image="images/BizimSokak.jpg"/>
  <Map Keyword="cilgin" Image="images/cilgin.jpg"/>
  <Map Keyword="firtinaoncesi" Image="images/firtinaoncesi.jpg"/>
  <Map Keyword="gunes" Image="images/gunes.jpg"/>
  <Map Keyword="kopru" Image="images/kopru.jpg"/>
  <Map Keyword="maldivler" Image="images/maldivler.jpg"/>
  <Map Keyword="meksikasahili" Image="images/meksikasahili.jpg"/>
  <Map Keyword="mistik" Image="images/mistik.jpg"/>
  <Map Keyword="sahil" Image="images/sahil.jpg"/>
  <Map Keyword="tatil" Image="images/tatil.jpg"/> 
</Mapper>

Görüldüğü gibi images klasörü altındaki her bir resim ve eş düşen kelime, Xml içeriğinde tanımlanmış durumda. Tabi bu benim minik hayal gücümün bir ürünü. Buradaki sistem dahada etkili geliştirilebilir. Söz gelimi kullanıcının girdiği kelimeye göre, sunucu tarafında çalışacak akıllı bir robot, resim kataloğundan, kelime bire bir uymasa bile en yakın olanı bulup istemciye gönderebilir. Bu kısmı siz değerli okurlarıma bırakayım Wink

Gelelim projenin kod yapısına. Burada WCF Rest Starter Kit kullandığımız için herhangibir REST şablonuna ait projelerden birisini oluşturmak yeterli. Önemli olan noktalardan birisi servis tarafındaki çalışma zamanı için WebServiceFactory2 isimli fabrikanın(Factory Class) kullanılmasıdır. O yüzden Servis' e ait markup içeriğinin aşağıdaki gibi olmasına özen göstermekte yarar vardır.

<%@ ServiceHost Language="C#" Debug="true" Service="Streaming.Service" Factory="Streaming.AppServiceHostFactory"%>

using System;
using System.ServiceModel;
using System.ServiceModel.Activation;
using Microsoft.ServiceModel.Web;
using Microsoft.ServiceModel.Web.SpecializedServices;

namespace Streaming
{
    class AppServiceHostFactory : ServiceHostFactory
    {
        protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses)
        {
            return new WebServiceHost2(serviceType, true, baseAddresses);
        }
    }
}

Servise ait kod içeriğini ise senaryoya göre aşağıdaki gibi geliştirdim.

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;
using System.Xml.Linq;
using Microsoft.ServiceModel.Web;

[assembly: ContractNamespace("", ClrNamespace = "Streaming")]

namespace Streaming
{
    [ServiceBehavior(IncludeExceptionDetailInFaults = true, InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Single)]
    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
    [ServiceContract]
    public class Service
    {       
        [WebHelp(Comment="Şansına göre bir duvar kağıdı döndürür")]
        [WebGet(UriTemplate="yourimage?keyword={keyword}")]
        [OperationContract]
        public Stream GetImage(string keyword)
        {
            // Özellikle ilk talep sırasında keyword değeri gelmeyeceği için istemci tarafına BadRequest tipinden Http hatası döndürülür
            // İstenirse varsayılan bir resimde ürettirilebilir
            if (String.IsNullOrEmpty(keyword))
                throw new WebProtocolException(HttpStatusCode.BadRequest);

            // gelen kelimeye eş düşen resim adresi Xml dökümanı içerisinden LINQ sorgusu ile çekilir
            XDocument doc = new XDocument();
            doc = XDocument.Load(System.Web.HttpContext.Current.Server.MapPath("~/Mapper.xml"));
            // TODO: Xml içeriğinde Keyword niteliğinde olmayan bir bilgi gelirse aşağıdaki sorgu patlar. Buna tedbir alınması ve uygun Http hatasının döndürülmesi önerilir.
            string imagePath = (from img in doc.Document.Elements("Mapper").Elements("Map")                            
                            where img.Attribute("Keyword").Value == keyword.ToLower()
                            select img.Attribute("Image").Value).First();

            // eğer kelimeye denk düşen resim yoksa NotFound tipinden Http hatası döndürülür
            if (String.IsNullOrEmpty(imagePath))
                throw new WebProtocolException(HttpStatusCode.NotFound);

            // Image tipi resim adresinden üretilir.
            Image image = Image.FromFile(System.Web.HttpContext.Current.Server.MapPath(imagePath));           
            // Çıktının içerik tipi jpeg formatı olarak belirlenir.
            WebOperationContext.Current.OutgoingResponse.ContentType = "image/jpeg";
            // AdapterStream yapıcısı kullanılarak stream istemci tarafına hazırlanıp gönderilir.
            return new AdapterStream(str => image.Save(str, ImageFormat.Jpeg));
        }
    }
}

Kodun belkide en basit ama en önemli kısmı AdapterStream tipinin üretildiği satırdır. Bu satıra kadarki kısımda, gelen talebe göre Xml dökümanı içerisinden, denk düşen resim adresinin elde edilmesi ve buna göre Image nesnesinin örneklenmesi işlemleri yapılır. Sonrasında ise çıktının tipi(image/jpeg) olarak belirlenir ve son adıma gelinir. AdapterStream sınıfının yapıcı metodu parametre olarak Action<Stream> veya Action<TextWriter> tipinden bir temsilci(delegate) almaktadır. Bu temsilci, geriye değer döndürmeyen(void) ve parametre olarak bir Stream veya TextWriter referansı alan metodları işaret edecek şekilde tanımlanmıştır. Elimizde =>(lambda) operatörü gibi bir yardımcıda olduğundan nesne örneklenmesi sırasında temsilcinin tanımlanması, işaret ettiği metodun gövdesinin yazılması aynı ifade içerisinde mümkün olmaktadır.

Peki ya sonuçlar?

Servisi tarayıcıdan ilk seferde parametre kullanmadan talep ettiğimde aşağıdaki ekran görüntüsü ile karşılaştım.

Bu son derece doğaldı. Nitekim bu talep sonrasında servis operasyonuna herhangibir keyword değeri gelmemektedir. Ancak tarayıcıdan, http://buraksenyurt:1000/Service.svc/yourimage?keyword=sahil gibi bir talepte bulunduğumda aşağıdaki sonucu elde ettim. Cool Sanırım şansıma deniz kıyısında şöyle güzel bir tatil çıktı.

Hemen arka planda çalışan Fiddler aracına baktığımdaysa, gelen talebe karşılık üretilen cevabın resim içerikli olarak üretildiğini gördüm.

Görüldüğü gibi AdapterStream kullanımı son derece kolay. Özellikle REST bazlı WCF servislerini tüketen web uygulamalarında göz önüne alınabilir. Umarım size faydalı bir bilgi daha aktarabilmişimdir. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Streaming.rar (5,62 mb) (Dosya içerisinde bir TODO var. Bu kısmı gözden kaçırmayın Wink )

Tags:   ,
Categories:   WCF
Actions:   E-mail | del.icio.us | Permalink | Yorumlar (0) | Comment RSSRSS comment feed
Bookmark and Share

WCF Rest Servislerinde SqlCacheDependency Kullanımı

Perşembe, 30 Nisan 2009 00:40 by bsenyurt

Merhaba Arkdaşlar,

WCF Rest Servislerinde Önbellekleme(Caching) başlıklı yazımda, Rest Starter Kit ile birlikte gelen WebCache niteliğini(attribute) kullanarak ön bellekleme profillerinin nasıl hazırlanabileceğini  ve kullanılabileceğini incelemeye çalışmıştım. Önbellekleme ile ilgili olaraktan özellikle SQL tarafını ilgilendiren konulardan biriside SqlCacheDependency kullanımıdır. Daha öncedende bahsettiğim gibi, Asp.Net Caching mekanizmasının bir kaç farklı uygulama biçimi vardır. Bunlardan birisi olan SqlCacheDependency mekanizmasına göre, ön bellekte tutulacak olan veri içeriğinin bir tabloda meydana gelecek değişikliklere bağımlı hale getirilmesi sağlanabilir. Bu oldukça etkili bir model olmasına rağmen sadece SQL Server destekli olması çoğu çevrede pek kullanışlı olarak varsayılmamaktadır. Ama yinede SQL sunucuları üzerinde çalışılan sistemlerde bu mekanizmadan nasıl yararlanılabileceğini bilmek önemlidir. Tahmin edeceğiniz gibi yazımın bu seferki ana konusu, REST tabanlı bir WCF servisinde, WebCache niteliğini kullanırken SqlCacheDependency modelini nasıl ele alacağımızı öğrenmektir. Ama öncesinde Sql sunucusu üzerinde Cache Dependency için bazı ön hazırlıklar yapılmalıdır. Öncelikli olarak veritabanının, sonrasında ise önbellek değişiklikleri için izlenecek olan tablonun aspnet_regsql aracı yardımıyla sisteme uygun hale getirilmeleri gerekmektedir.

Aslında sakin kafayla düşündüğümüzde, bir tabloda meydana gelecek değişiklerin izlenmesi için ilk olarak bir trigger yazacağımız ortadadır. Veri eklenmesi, güncellenmesi veya silinmesi sırasında devreye girecek olan bu trigger, değişiklikleri ayrı bir tabloya loglayarak başka sistemlerin(örneğin Asp.Net Cache Modüllerinin) kullanımına açabilir. Değişikliklerin trigger içerisinden aktarımı sırasında dahili sql ifadeleri kullanılabileceği gibi, bu işi üstlenen bir Stored Procedure' de söz konusu olabilir. aspnet_regsql aracının yaptığıda aslında tam olarak bu düşünce yapısına benzerdir. Lafı fazla uzatmıyayım Embarassed Visual Studio 2008 Command Prompt üzerinden aspnet_regsql aracını aşağıdaki ekran görüntüsünde olduğu gibi çalıştırabiliriz.

İlk olarak Northwind veritabanının Cache Dependency için hazırlandığını görüyoruz. Burda -S ile sunucu adını(server name), -E ile işlemi windows' u açan kullanıcı yetkisi ile yapacağımızı belirtirken -d parametresinden sonra veritabanı(database) adını işaret ediyoruz. -ed' nin anlamı ise enable database Smile 

İkinci olarak Products tablosu için gerekli Cache Dependecy hazırlıklarını yapan komutu çalıştırmaktayız. Burada ise bir öncekinden farklı olark -et ile enable table diyor(bu sayede tablo için cache dependency' yi açacağımızı belirtiyoruz) ve -t ilede tablo adını(table name) ifade ediyoruz.

Bu işlemlerin ardından Northwind veritabanını biraz incelediğimizde Products tablosuna Products_AspNet_SqlCacheNotification_Trigger isimli bir trigger eklendiğini ve içerisinde AspNet_SqlCacheUpdateChangeIdStoredProcedure isimli bir sp çağırılarak Products parametresi gönderildiğini görebiliriz. Sp' nin yaptığı ise AspNet_SqlCacheTablesForChangeNotification tablosuna gerekli bilgileri eklemek. Tabiki veritabanı içerisinde izlenecek tablolar için gerekli trigger' ların oluşturulmasını sağlayan sp gibi farklı amaçlarla kullanılan nesnelerde oluştuğu gözlemlenebilir. Gelelim bizi ilgilendiren kısma. Daha önceki yazımızda geliştirdiğimiz Atom formatından içerik yayınlaması yapan servisin Caching mekanizması için Products tablosuna bağımlı olacak şekilde bir profile bildirimi yapmamız gerekiyor. Ki bu tek başına yeterli olmayacaktır. Aslında web.config dosyası içerisindeki değişiklikler üzerinden konuşursak çok daha doğru olacak. Bu amaçla web.config dosyası içeriğini aşağıdaki gibi düzenlediğimizi düşünelim.

web.config içeriği;

<?xml version="1.0"?>
<configuration>
  <connectionStrings>
    <add name="NorthwindConStr" connectionString="data source=localhost; database=Northwind;integrated security=SSPI"/>

  </connectionStrings>
 <system.web>
    <caching>
      <outputCacheSettings>
        <outputCacheProfiles>
            <clear/>
            <add name="Cache1" duration="60" enabled="true" location ="Server" varyByParam="size"/>
            <add name="Cache2" duration="20" enabled="true" location ="Client" varyByParam="none"/>
            <add name="Cache3" sqlDependency="Northwind:Products" varyByParam="size" enabled="true" location="Any"/>
        </outputCacheProfiles>       
      </outputCacheSettings>
      <sqlCacheDependency enabled="true">
        <databases>
          <add name="Northwind" connectionStringName="NorthwindConStr" pollTime="10000"/>
        </databases>
      </sqlCacheDependency>
    </caching>
  <compilation debug="true"/>
        </system.web>
 <system.serviceModel>
  <serviceHostingEnvironment aspNetCompatibilityEnabled="true"/>
 </system.serviceModel>
</configuration>

İlk olarak Northwind veritabanı için bir connectionString tanımlaması yapıldığını görmekteyiz. Bu tanımlama, ilerleyen kısımlarda yer alan sqlCacheDependency elementi içerisinde kullanılmaktadır. Yani Cache Dependency sisteminin hangi SQL bağlantısı üzerinden yürütüleceğini belirtmiş oluyoruz. Diğer taraftan sqlCacheDependency elementi altında pollTime isimli bir nitelik tanımlaması yapıldığını görmekteyiz. Buna göre her 10 saniyede 1, connectionStringName ile belirtilen bağlantıda yer alan ilgili tabloda bir değişiklik olup olmadığı kontrol edilecektir. Polling kelimesinin buradaki mantığıda zaten budur. (Git bak, duruma göre bir şeyler yap Smile ) Bu ayarlamalar aslında standart olarak Asp.Net tarafında geliştirilen web uygulamalarındakiler ile aynıdır. Ekstra olan, eklenen Cache3 isimli önbellekleme profil içeriğidir. Bu içerikte sqlDependency özelliğine atanan değer ile Northwind isimli cache dependency için Products tablosuna bağlı bir önbellekleme sistemi kullanılacağı belirtilmektedir. Ayrıca önbellekleme yapısı size parametresine bağımlı hale getirilmiştir. Bundan sonra tek yapılması gereken WebCache niteliğini aşağıdaki gibi tanımlaktır.

Nasıl test edeceğim?

Tabiki sistemi nasıl test edeceğimizi konuşmamızda yarar var. Ben hemen kendi testimi nasıl yaptığımı anlatayım ki muhtemelen sizlerde benzer bir testi yapacaksınız. İlk etapta yukarıdaki şekilden de görüldüğü gibi GetFeed metodu içerisine bir breakpoint koydum. Uygulamayı debug modda çalıştırdığımda ilk etapta kod içerisine düşüldü ve sonuçlar ekrana geldi. Daha sonra örnek olarak stok miktarı 10 un altında olan ürünleri Atom formatında çekmek için http://localhost:1000/ProductFeedService.svc/?size=10 talebini gönderdim. Doğal olarak parametre bazlı bir önbellekleme profili kullandığımdan ve daha önceden bu talebi göndermediğimden kod içerisine tekrardan düştüm. Ancak sonraki taleplerde herhangibir şekilde koda düşmediğimi tespit ettim.(Hatta bu sırada bloğun üst tarafındaki resimdekine benzer bir kahve almak için mutfağa gittim, kahvemi hazırladım. Derken bir arkadaşımı gördüm ve onunla bir süre sohbet ettim. Sonrada işten çıktım ve eve gittim. Aradan saatler geçtikten sonra aklıma geldi örnek ve tekrardan aynı talebi gönderdim Cool ) Bu içeriğin ön bellekten getirildiğinin zaten bir kanıtıydı. Sonrasında ise stok miktarı 10 un altında olan satırlardan birisinde bir değişiklik yaptım. Örnek olarak Chef Anton's Gumbo Mix isimli ürünün adını Chef Anton's Gumbo Mix... olarak değiştirdim. Bu durumda tarayıcıdan yine http://localhost:1000/ProductFeedService.svc/?size=10 talebini gönderdiğimde tekrardan kod içerisine düştüğümü gözlemledim. Bir başka deyişle Products tablosunda değişiklik olduğu için Polling süresine göre caching mekanizması devreye girdi ve verinin tekrardan getirilmesine karar verdi. İşte bu kadar...

Böylece geldik bir yazımızın daha sonuna tekrardan görüşünceye dek hepinize mutlu günler dilerim.

CachingDependency.rar (109,00 kb)

Tags:   ,
Categories:   WCF
Actions:   E-mail | del.icio.us | Permalink | Yorumlar (0) | Comment RSSRSS comment feed
Bookmark and Share

nedirtv?com - Ankara Seminerleri

Salı, 28 Nisan 2009 16:57 by bsenyurt

nedirtv?com, 3. yıldönümü etkinliklerine devam ediyor. 3 Mayıs 2009 Pazar günü Ankara Bilkent Üniversitesi’nde değerli Ankaralılarla beraber olacağız. INETA Türkiye ve Bilkent ACM Student Chapter desteğiyle gerçekleştireceğimiz bu tüm günlük etkinliğe tüm Ankaralı yazılımcıları bekliyoruz.

Not: Seminerlere katılım ücretsizdir.

Seminer Programı:
09:30 Açılış – nedirtv?com Tanıtımı
09:45 ASP.NET MVC (Uğur Umutluoğlu)
11:30 What is SharePoint? (Burak Batur)

13:00 Ara

14:00 WCF 4.0 & WF 4.0 (Burak Selim Şenyurt)
15:30 WPF ve Multitouch Development (Daron Yöndem)
17:00 Bitiş


Zaman:
3 Mayıs 2009 Pazar

Yer:
Merkez Kampüs Rektörlük Binası Mithat Çoruh Amfi Salonu, Bilkent Üniversitesi - ANKARA

Tags:  
Categories:   WCF
Actions:   E-mail | del.icio.us | Permalink | Yorumlar (0) | Comment RSSRSS comment feed
Bookmark and Share

WCF Rest Servislerinde Önbellekleme(Caching)

Pazartesi, 27 Nisan 2009 21:28 by bsenyurt

Merhaba Arkadaşlar,

REST(REpresentational State Transfer) modelini uygulayan WCF servislerinin geliştirilmesinde, WCF Rest Starter Kit ile birlikte gelen kolaylıklardan biriside, önbellekleme(Caching) işlemlerinin dekleratif(Declarative) olarak yapılabilmesidir. Burada dekleratiflikten kastımız, önbellekleme bildirimlerinin çalışma zamanına nitelik(Attribute) yoluyla bildirilmesidir. Web programlama modeline göre geliştirilen servislerin çeşidi ne olursa olsun, performans kriterleri söz konusu olduğunda önemli olan noktalardan biriside verilerin istemciye gönderilmeden önce gerekiyorsa belirli süreler boyunca veya bazı koşullar sağlanıncaya kadar sunucu ön belleğinde(hatta bazen istemci tarafında tarayıcı uygulama için ayrılan özel bölgelerde) tutulmasıdır. Özellikle Asp.Net web uygulamalarını göz önüne alırsak, ön bellekleme ile ilişkili olarak kullanılan modüllerin pek çok farklı yapıyı desteklediğini görürüz. Söz gelimi, absolute caching(Kesin bir süre kadar önbellekte tutulması), sliding expiration caching(belirli süre içerisinde talep geldikçe önbellekte tutulma süresinin ileriye ötelenmesi), file dependency caching(önbellekteki verinin yenilenme koşulunun dosyadaki değişimlere bağlanması),sql dependency caching(ön bellekteki verinin yenilenme koşulunun bir tablodaki değişimlere bağlanması) vb... Hal böyle olunca, genellikle Asp.Net destekli sunucularda host edilen WCF servislerindende bu hazır Caching modüllerinden yararlanılması kaçınılmazdır. Startet Kit ise, Microsoft.ServiceModel.Web assembly' ı içerisinde sunduğu WebCache niteliği ile bu özelliğin REST bazlı WCF servislerinede uygulanabilmesini, olanaklı kılmaktadır.

Bu yazımızda söz konusu ön bellekleme sisteminin Atom formatında içerik yayınlaması(Atom Feed Syndication) yapan bir WCF Rest servisinde nasıl ele alınabileceğini incelemeye çalışacağız. İşe ilk olarak Atom Feed WCF Service şablonunda bir proje açarak başlayabiliriz.

Atom Feed WCF Service proje tipi, tahmin edeceğiniz üzere WCF Rest Starter Kit ile birlikte Visual Studio 2008 ortamına yüklenen hazır şablonlardan birisidir. Şablon içeriği zaten hazır bir kod yapısına sahiptir ve gerekli TODO işaretlemeleri ile geliştiriciye neler yapması gerektiğini varsayılan ölçülerde bildirmektedir. Ancak bundan daha da önemlisi şablonun kullanım amacıdır. Bu şablon, ATOM formatında içerik yayınlaması yapan bir WCF servis operasyonunun REST bazlı olacak şekilde sunulabilmesini sağlamaktadır. Bu sebeten hazır olarak sunulan GetFeed isimli operasyonun dönüş tipi Atom10FeedFormatter nesne örneğindendir. Servis kodlarını tamamlamadan önce web.config dosyası içerisinde önbellekleme için gerekli profil özelliklerini aşağıdaki şekilde görüldüğü gibi belirleyebiliriz.

Buradaki ayarlamalara göre iki farklı önbellekleme profili tanımlanmaktadır. Cache1 isimli profile göre, önbellekleme süresi 60 saniyedir ve sunucu taraflı bir önbellekleme yapılacağı location niteliğine atanan Server değeri ile belirtilmektedir. Diğer taraftan Cache2 isimli profilde ise sadece süre ve lokasyon bilgileri farklıdır. Konfigurasyon dosyasında önem arz eden konulardan biriside aspNetCompatibilityEnabled niteliğine atanan true değeridir. Asp.Net ortamının önbellekleme modülünden yararlanılacağı için bu değerin true olması önemlidir.

Artık bizim yapmamız gereken tek şey, servis tarafında kullanacağımız WebGet niteliğine hangi Cache profilini kullanacağını bildirmektir. Buna göre başlangıçta FeedService adıyla oluşturulmuş olan ama örneğimizde ProductFeedService ismiyle tanımlanan servisimize ait kod içeriğini aşağıdaki gibi değiştirebiliriz.

using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Globalization;
using System.Net;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Syndication;
using System.ServiceModel.Web;
using Microsoft.ServiceModel.Web;

namespace Caching
{   
    [ServiceBehavior(IncludeExceptionDetailInFaults = true), AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed), ServiceContract]
    public partial class ProductFeedService
    {
        [WebHelp(Comment = "Stok bilgisine göre tüm ürün bilgileri Atom formatında getirilir.")]
        [WebGet(UriTemplate = "?size={stockSize}")]
        [WebCache(CacheProfileName="Cache1")] // web.config dosyasında, Cache1 isimli önbellekleme profilininin takip eden operasyon için kullanılacağı belirtilir.
        [OperationContract]
        public Atom10FeedFormatter GetFeed(short stockSize)
        {           
            SyndicationFeed feed;

            // varsayılan kontroller

            if (stockSize < 0)  // stok değeri parametresi 0' ın altında ise
                throw new WebProtocolException(HttpStatusCode.BadRequest, "Stok miktarı eksi değer olamaz.", null);
            // stok değeri parametresi girilmemişse(ki servis querystring kullanılmadan talep edilirse bu çalışır)
            if (stockSize == 0)
                stockSize = 10;

            // Kritere uyan her bir Product için birer SyndicationItem örneği oluşturulup, SyndicationItem tipinden generic List koleksiyonuna eklenir.
            List<SyndicationItem> items = new List<SyndicationItem>();
            foreach(Product product in GetProducts(stockSize))
            {
                items.Add(new SyndicationItem()
                {
                    // Syndication içeriğinde olması gereken standart bazı özelliklerin değerleri set edilir.

                    Id = String.Format(CultureInfo.InvariantCulture, "http://northwind.com/ProductID{0}", product.ProductId),
                    Title = new TextSyndicationContent(String.Format("'{0}' ürünü", product.Name)),                   
                    LastUpdatedTime = DateTime.Now,                   
                    Authors = {new SyndicationPerson() {Name = ""} // Yazar bilgisi kullanılmadığı için boş bırakıldı
                    },
                   
                    Content = new TextSyndicationContent(String.Format("{0}, {1}, {2}, {3}",product.ProductId.ToString(),product.Name,product.UnitPrice.ToString("C2"),product.UnitsInStock.ToString())),
                     PublishDate=DateTime.Now,
                      Summary=new TextSyndicationContent(String.Format("{0} isimli üründen stokta {1} adet bulunmaktadır",product.Name,product.UnitsInStock.ToString())),                     
                });
            }
           
            // Feed hazrılanır ve Items özelliğine, List<SyndicationItem> tipinden olan ve yukarıda hazırlanan itemsi isimli koleksiyon atanır.
            feed = new SyndicationFeed()
            {
                Id = "http://northwind.com/ProductsWithStock",
                Title = new TextSyndicationContent("Stok miktarı bazlı ürün listesi"),
                Items = items
            };
            feed.AddSelfLink(WebOperationContext.Current.IncomingRequest.GetRequestUri());       
            // Operasyonun çıktı formatının Atom olacağı ContentType özelliğine atanan değer ile belirlenir
            WebOperationContext.Current.OutgoingResponse.ContentType = ContentTypes.Atom;           
            // syndication içeriği operasyondan geriye döndürülür.
            return feed.GetAtom10Formatter();
        }

        // Yardımcı metod. UnitsInStock değeri stockSize ile gelen değerin altında olan ürünleri Product tipinden bir koleksiyon olarak geriye döndürmektedir.
        private List<Product> GetProducts(short stockSize)
        {
            List<Product> products = new List<Product>();
            using (SqlConnection conn = new SqlConnection("data source=.;database=Northwind;integrated security=SSPI"))
            {
                SqlCommand cmd = new SqlCommand("Select ProductID,ProductName,UnitPrice,UnitsInStock From Products Where UnitsInStock<@UnitsInStock", conn);
                cmd.Parameters.AddWithValue("@UnitsInStock", stockSize);
                conn.Open();
                SqlDataReader reader = cmd.ExecuteReader(System.Data.CommandBehavior.CloseConnection);
                while (reader.Read())
                {
                    products.Add(
                        new Product
                        {
                             ProductId=Convert.ToInt32(reader["ProductID"]),
                              Name=reader["ProductName"].ToString(),
                               UnitPrice=Convert.ToDecimal(reader["UnitPrice"]),
                                UnitsInStock=Convert.ToInt16(reader["UnitsInStock"])
                        }
                        );
                }
                reader.Close();
            }
            return products;
        }
    }
}

GetFeed metodu Products tablosundan, stok miktarı parametre olarak gelen stockSize değerinden düşük olan ürün bilgilerini Atom formatından bir içerik olarak istemciye göndermektedir. Burada verinin çekilmesi sırasında yardımcı bir metod olarak GetProducts fonksiyonu kullanılmaktadır. Elbetteki yazımızın konusu itibariyle önem arz eden tek satır WebGet niteliğinin kullanıldığı yerdir. Niteliğin CacheProfileName özelliğine atanan değer ile, GetFeed metodunun üreteceği çıktının hangi kriterlere göre nasıl ön bellekleneceği belirtilmektedir. Servis bu haliyle talep edildiğinde ve örneğin stok miktarı 15 birimin altında olan ürün listesi istendiğinde, aşağıdaki ekran görüntüsüne benzer bir çıktı ile karşılaşılabilir.

Tabi bu görüntü yazımız için yeterli değildir. Nitekim amacımız ön bellekleme sistemini test edebilmek. Burada izlenebilecek bir kaç yol var. En kolayı geliştiricinin GetFeed metodu başına breakpoint koyarak debug modunda ilerlemesidir. Bu durumda, servise gelen ilk talep ile birlikte GetFeed metodunun üreteceği içerik 60 saniyeliğine(duration=60) sunucunun ön belleğinde tutulamaya başlancaktır. Bu sırada tarayıcı üzerinden aynı sayfa tekrardan talep edilirse metodun içerisinde düşülmediği ve az önce üretilen çıktının geldiği gözlemlenecektir. Tabi burada çok dikkat edilmesi gereken bir nokta vardır. Servis bu haliyle talep edildiğinde, stockSize değeri 0 olduğundan if koşuluna göre 10 birimin altında olanlar getirilmektedir. Şayet bundan sonra tarayıcıdan http://localhost:1000/Service.svc/?size=100 gibi bir talep girilirse yine stok miktarı 10 birimin altında olanlar getirilecektir. Neden? Tabiki ilk talep önbellekte tutulduğu için Wink Elbette, 60 saniyelik ön bellekleme süresi beklendikten sonra bu talep gönderilirse, stok miktarı 100' ün altında olan ürünlerin elde edilebildiği ve doğal olarak debug moddayken ,GetFeed metodu içerisine düşülebildiği gözlemlenecektir. Bu tam olarak istenen bir şey olmayabilidir aslında. Belkide size parametresine göre ayrı ayrı ön bellekleme yapılabilmesi daha doğru olabilir ki buda son derece basittir. Tek yapılması gereken ön bellekleme profilini aşağıdaki gibi güncelleştirmektir.

Görüldüğü gibi tek yaptığımız varyByParam niteliğine size değerini vermektir. Önbellekleme ile ilişkili detayları web.config dosyası içerisinde taşımanın en büyük avantajlarından biriside, koda girmeden değiştirilebilme olanağı sağlamasıdır. Örneğin ürünün IIS altına atılmasından sonra, size parametresine göre önbellekleme yapılacağına karar verildiyse eğer, kodu tekrardan açmadan web.config dosyasına müdahale edilerek bu güncelleme gerçekleştirilebilir. Diğer yandan istenirse web.config dosyası kullanılmadan, WebGet niteliğinin özellikleri içerisindede önbellekleme bilgileri tanımlanabilir.

[WebHelp(Comment = "Stok bilgisine göre tüm ürün bilgileri Atom formatında getirilir.")]
[WebGet(UriTemplate = "?size={stockSize}")]
[WebCache(Duration=60, Location=System.Web.UI.OutputCacheLocation.Any, VaryByParam="size")]
[OperationContract]
public Atom10FeedFormatter GetFeed(short stockSize)
{

şeklinde...

Evettt..Böylece geldik bir blog yazımızın daha sonuna. Bu kısa yazımızda WCF REST Starter Kit' i kullanarak bir Atom Feed WCF Service' in nasıl geliştirilebileceğini gördük. Ama dahada önemlisi, WebGet niteliği yardımıyla ön bellekleme işlemlerinin nasıl yapılabileceğine bakmaya çalıştık. Görüşmek dileğiyle...

Örnek Dosya; Caching.rar (107,66 kb)

Tags:   ,
Categories:   WCF
Actions:   E-mail | del.icio.us | Permalink | Yorumlar (0) | Comment RSSRSS comment feed
Bookmark and Share