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

Silverlight Tarafında HTTP Bazli Servisleri Kullanmak

Pazartesi, 12 Temmuz 2010 09:55 by bsenyurt

Merhaba Arkadaşlar,

Eğitmenlik yaptığım yıllarda Microsoft' un ders kitaplarında yer alan LAB çalışmalarını mümkün mertebe yapmaya ve yaptırmaya çalışırdım. Hatta çoğu zaman eğitimlere hazırlanırken sık sık bu lab çalışmalarını kendim yapar ve hatta ek ilaveler ile daha da eğlenceli hale getirmeye çalışırdım. Tabi bazen elimizde lab yapacağımız kitaplarımız olmazdı ki o ayrı bir hikaye. Lab çalışmaları öğrencinin adım adım yapması gerekenleri söylerek, konunun en yalın haliyle anlaşılmasını sağlamakta önemli rol oynamaktadır. Lab çalışmalarındakine benzer konu anlatımları benimde özümsediğim ve faydalı bulduğum öğrenme tekniklerinden birisidir. İşte bu yazımızda da bu kültüre uymaya çalışarak ilerlemeye çalışıyor olacağız. Hedefimiz Silverlight uygulamalarından, HTTP tabanlı taleplere göre operasyonel hizmetlerde bulunan servisleri nasıl kullanabileceğimizi, en yalın haliyle görmek. Haydi o zaman lab için gerekli materyalleri değerlendirerek yola koyulalım.

Adım 0 : Mevzumuz

Bilindiği üzere bazı servisler HTTP protokolü üzerinden GET, POST, PUT veya DELETE metod çağrıları ile kullanılabilmektedir. Bu anlamda WCF Eco System içerisinde yer alan WebHTTP servisleri, söz konusu tipteki hizmetleri sunmak üzere WCF alt yapısı üzerine oturmuş bir model sunmaktadır. Çok doğal olarak Silverlight tabanlı istemciler de bu servislerin tüketicileri olabilirler. Bu tip servislerin kullanıldığı senaryolarda istemci tarafında herhangibir Proxy tipi söz konusu olmadığı için, HTTP GET,POST,PUT veya DELETE metodlarının manuel olarak hazırlanması ve gönderilmesi gerekmektedir. Silverlight tarafında bu işlemler için WebClient veya HttpWebRequest tiplerinden yararlanılabilmektedir. Biz bu yazımızda WebClient tipinden yararlanarak, IIS(Internet Information Services) üzerinde konuşlandırılmış basit bir WebHttp Service örneğinin nasıl kullanılabileceğini incelemeye çalışıyor olacağız.

Adım 1 : WCF Rest Application Uygulaması ve Entity Data Model' in Oluşturulması

İşe ilk olarak WCF Rest Service Application şablonunda bir proje oluşturarak başlayabiliriz. Bildiğiniz üzere bu proje şablonu(Project Template) hali hazırda yüklü değilse Online Template' ler arasından install etmeniz gerekmektedir. Söz konusu örnekte Chinook veritabanında yer alan ve çok basit olarak ilerlemek istediğimizden sadece Album tablosunu içeren bir Entity Data Model kullanabiliriz. Aşağıdaki şekilde örneğimizde kullanmakta olduğumuz Entity Data Model yer almaktadır.

 

Adım 2 : WebHttp Service Örneğinin Geliştirilmesi

Entities isimli WCF WebHttp Service sınıfımızın içeriğini ise aşağıdaki gibi düzenlediğimizi düşünebiliriz.

using System.Collections.Generic;
using System.Linq;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;

namespace ChinookDataPortal
{
    [ServiceContract]
    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
    public class Entities
    {
        [WebGet(UriTemplate = "Albums/All")]
        public List<Album> GetAlbums()
        {
            List<Album> albums = null;

            ChinookEntities entities = new ChinookEntities();
            albums=(from albm in entities.Albums
                    orderby albm.Title
                    select albm).ToList();

            return albums;
        }

        [WebGet(UriTemplate="Albums/{firstLetter}")]
        public List<Album> GetAlbumsByFirstLetter(string firstLetter)
        {
            List<Album> albums = null;

            ChinookEntities entities = new ChinookEntities();
            albums = (from albm in entities.Albums
                      where albm.Title.ToLower().StartsWith(firstLetter.ToLower())
                      orderby albm.Title
                      select albm).ToList();

            return albums;
        }
    }
}

Servis tipimiz iki operasyon içermekte olup her ikiside HTTP Get çağrılarına cevap verecek şekilde düzenlenmişlerdir. GetAlbums metoduna yapılan çağrılarda servis URL adresine Albums/All takısı eklenmelidir. Diğer yandan ilk harflerine göre albümleri listeleyen GetAlbumsByFirstLetter metodu, URL adresine Albums/{firstLetter} bilgisinin eklenmesini beklemektedir. Her iki metod ChinookEntities tipini kullanmakta ve basit LINQ sorguları ile sonuç üretmektedir. Servisimizi bu şekilde geliştirdikten sonra IIS altına Publish ederek devam edebiliriz.

Adım 3 : IIS Publish

Publish işlemleri için aşağıdaki şekilde görülen Profile ayarlarını kullanabilirsiniz.

Bu ayarlara göre servisimizin IIS üzerinde yer alan Default Web Site isimli Application Pool altına dağıtılacağı belirtilmiş olunur.

Not: IIS üzerinden Convert To Application işlemini yapmanız gerekebilir.

Sonuç olarak IIS içerisinde aşağıdaki gibi servisin üretilmiş olması gerekmektedir. Bu arada örneği geliştirdiğimiz makinede Windows 7 Enterprise işletim sisteminin ve IIS 7.5.7600.16385 sürümünün olduğunu belirtelim.

Bu noktadan sonra Silverlight uygulamasının geliştirilmesi aşamına geçilecektir. Ancak öncelikle gerekli testleri yapılmasında yarar vardır.

ChinookDataPortal.rar (46,54 kb) [Örnek Visual Studio 2010 Ultimate RC ortamında geliştirilmiş ve test edilmiştir]

Adım 4 : WebHttp Service Test

Silverlight tarafındaki uygulamamızı geliştirmeden önce servisimizi IIS üzerinden test etmemizde ve çalıştığından emin olmamızda yarar olacağı kanısındayım. İlk olarak yardım sayfasına ulaşıp ulaşamadığımızı öğrenelim. Bilindiği üzere WebHttp Service örnekleri aksi belirtilmedikçe hazır bir yardım sayfası sunmaktadır. Bu amaçla tarayıcı uygulamadan http://localhost/ChinookDataPortal/Entities/help şeklinde bir talepte bulunduğumuzda, aşağıdaki ekran çıktısı ile karşılaşmış olmalıyız.

Yardım sayfasının çalışıyor olması dışında servis tarafında yer alan operasyonel metodların da test edilmesinde yarar vardır. Örneğin tüm albümleri elde etmek için http://localhost/ChinookDataPortal/Entities/Albums/All şeklinde talepte bulunduğumuzda, aşağıdaki ekran görüntüsünde yer alan sonuçları elde etmiş olmamız gerekmektedir. Tabi veri içeriklerinde değişiklikler söz konusu olabilir. Ancak XML çıktısının şematik yapısının benzer olması gerekmektedir.

Son olarak örneğin Cake adı ile başlayan albümleri çekmek istediğimizi ve bu amaçla URL satırından http://localhost/ChinookDataPortal/Entities/Albums/Cake şeklinde bir talep gönderdiğimizi düşünelim. Bu durumda ekran çıktısının aşağıdakine benzer olması gerekmektedir.

Eğer bu sonuçları elde edebiliyorsak servisimizin çalıştığını ve Sliverlight tarafı için kullanılabilir olduğunu söyleyebiliriz. Lakin dikkat etmemiz gereken bir nokta daha vardır.

Adım 5 : Client Access Policy Ayarları

Silverlight uygulamamızın farklı bir Domain içerisinde host edilmesine karşılık, IIS üzerinde gerekli Client Access Policy ayarlarının bulunması gerekmektedir. Bu nedenle IIS root klasörü altında yer alması gereken ClientAccessPolicy.xml dosyasının içeriğini aşağıdaki gibi düzenleyebiliriz.

<?xml version="1.0" encoding ="utf-8"?>
<access-policy>
  <cross-domain-access>
    <policy>
      <allow-from>
        <domain uri="*" />
      </allow-from>
      <grant-to>
        <resource path="/WorldWeatherService" include-subpaths="true"/>
        <resource path="/ChinookDataPortal" include-subpaths="true"/>
      </grant-to>
    </policy>
  </cross-domain-access>
</access-policy>

Burada görüleceği üzere ChinookDataPortal ve alt yollarına erişim izni verilmiştir. Artık Silverlight tarafını geliştirmeye başlayabiliriz.

Adım 6: Silverlight Application Projesinin Oluşturulması

Bu amaçla Visual Studio 2010 ortamında ConsumingHTTPBasedServices isimi ve Silverlight 4.0 tabanlı bir Application oluşturduğumuzu düşünelim. Söz konusu uygulamada RIA Service kullanılmayacağı için bu seçeneği pasif olarak bırakabiliriz. Bu işlem sonucu oluşturulan MainPage sayfasına ait XAML içeriğini ise aşağıdaki gibi geliştirebiliriz.

Adım 7 : MainPage.Xaml içeriği ve Kodun Yazılması

<UserControl x:Class="ConsumingHTTPBasedServices.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="517">

    <Grid x:Name="LayoutRoot" Background="White">
        <ListBox Height="203" HorizontalAlignment="Left" Margin="8,70,0,0" Name="AlbumListBox" VerticalAlignment="Top" Width="497" />
        <StackPanel Height="54" HorizontalAlignment="Left" Margin="9,10,0,0" Name="ButtonsStackPanel" VerticalAlignment="Top" Width="496" Orientation="Horizontal" />
    </Grid>
</UserControl>

MainPage içerisinde yer alan ListBox kontrolü içeriği A' dan Z' ye harfler ile doldurulacaktır. Herhangibir harfe basıldığında, WebHttp Service' imiz için bir HTTP Get talebi oluşturulacak ve sonuçların ListBox içerisinde gösterilmesi sağlanacaktır. Bu amaçla kod içeriğini aşağıdaki gibi geliştirmemiz yeterlidir.

using System;
using System.Linq;
using System.Net;
using System.Windows.Controls;
using System.Xml;
using System.Xml.Linq;

namespace ConsumingHTTPBasedServices
{
    public partial class MainPage
        : UserControl
    {
        // WebHttp Servisine basit HTTP metodları ile talepte bulunabilmemizi sağlayan WebClient nesnesi tanımlanır
        WebClient client;

        public MainPage()
        {
            InitializeComponent();

            // WebClient nesnesi örneklenir
            client=new WebClient();
            // Belirtilen URL adresine yapılan talep sonucu gerçekleşecek okuma işlemi tamamlandığında(bir başka deyişle veri istemci tarafında indirildiğinde) devreye girecek olan olay metodu tanımlanır.
            client.OpenReadCompleted += new OpenReadCompletedEventHandler(client_OpenReadCompleted);

            // A...Z Button üretimleri gerçekleştirilir
            for (int i = 65; i < 91; i++)
            {
                Button btn = new Button();
                btn.Width = 18;
                btn.Height = 18;
                btn.FontSize = 10;
                btn.Content = ((char)i).ToString();
                ButtonsStackPanel.Children.Add(btn);
                // Herhangibir Button tıklandığında
                btn.Click += (o, e) =>
                {                  
                    // Önce WebHttp Service' ne doğur yapılacak HTTP Get talebi için gerekli URI oluşturulur
                    Uri address = new Uri(String.Format("http://localhost/ChinookDataPortal/Entities/Albums/{0}", ((Button)o).Content));
                    // Belirtilen URI talebi asenkron olarak çalışan OpenReadAsycn metodu ile gönderilir
                    client.OpenReadAsync(address);                   
                };
            }
        }

        // URI ile belirtilen adres talebi gerçekleştirilip ilgili veri içeriği istemci tarafına indirildikten sonra devreye giren olay metodudur
        void client_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e)
        {
            // İçerik bir Stream olarak gelmektedir ve tasarlanan ChinookDataPortal WebHttp Servisi varsayılan olarak XML içerik göndermektedir.
            // Bu sebepten Stream XmlReader ile okunur
            XmlReader xReader = XmlReader.Create(e.Result);
            // XLINQ sorgusunun yapılabilmesi için XElement.Load metodu parametre olarak Stream' i kullanan XmlReader nesne örneğini alır
            XElement xElement = XElement.Load(xReader);
            // XLINQ sorgusu ile Title elementleri çekilir. XName.Get metodunun ikinci parametre XML Namespace' inin adıdır.
            var titles = from x in xElement.Elements().Elements(XName.Get("Title", "http://schemas.datacontract.org/2004/07/ChinookDataPortal"))
                         select x.Value;
            // Çekilen veri içeriği ListBox kontrolünün ItemsSource özelliğine bağlanır
            AlbumListBox.ItemsSource = titles;
        }
    }
}

Not: XElement tiplerini kullanabilmek ve XLINQ sorgularını yazabilmek için, Silverlight uygulamasına(ConsumingHTTPBasedServices.Web uygulamasına değil) System.Xml.Linq.dll Assembly' ının referans edilmesi gerekmektedir.

Adım 8 : Silverlight Uygulamasının Test Edilmesi

Dilerseniz uygulamanın çalışma zamanı sonuçlarına hemen bakalım. Böylece çalışma zamanı testlerini yapmış oluruz. Örneğin A başlıklı Button kontrolüne bastığımızda, aşağıdaki ekran görüntüsündekine benzer sonuçları almış olmalıyız. Yani Title alanındakilerden A harfi ile başlayanların listesinin elde edilebiliyor olması gerekmektedir.

Görüldüğü üzere ListBox içeriği baş harfi A olan albüm adları ile doldurulmuştur. Hemen bu işlemin arkasından örneğin C başlıklı Button kontrolüne basarsak aşağıdaki sonuçlar ile karşılaştığımız görürüz.

Süper değil mi? Wink

Özet

Tabi bu örnekte dikkat edilmesi gereken noktalardan birisi de, istemci tarafında herhangibir Proxy tipinin olmayışıdır. Bunun yerine HTTP Get metodu ile talepte bulunulmuş ve elde edilen Stream üzerindeki XML içeriği değerlendirilmiştir. Diğer yandan çok doğal olarak Servis tarafında kullanılan Entity Data Model içerisindeki tiplerin istemci tarafındaki karşılıkları bulunmamaktadır. Eğer bu tiplerin istemci tarafında ele alınması arzu edilirse açık bir şekilde oluşturulmaları gerekecektir. Tabi böyle bir senaryoda gelen XML veya JSON tipindeki içeriğinde ilgili tiplere dönüştürülmesi gibi bir işlem söz konusu olacaktır.

Ödev Smile

  1. Servisin XML yerine JSON(JavaScript Object Notation) formatında bir çıktı vermesi halinde, Silverlight tarafında gerekli olan kod düzenlemelerini yapınız.
  2. Servis üzerinden HTTP Put metod ile güncelleme işlemi yapabilmenizi sağlayacak bir geliştirmeyi aynı örnek üzerinden yapmaya çalışınız.

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

ConsumingHTTPBasedServices_RTM.rar (273,75 kb)[Örnek Visual Studio 2010 Ultimate RC Sürümü üzerinde geliştirişmiş ve RTM sürümü üzerinde test edilmiştir]

WCF Web Http Services - ETags

Cuma, 9 Nisan 2010 14:05 by bsenyurt

Merhaba Arkadaşlar,

WCF WebHttp Service' leri ile ilişkili yazılarımıza kaldığımız yerden devam ediyoruz. Bu yazımızda ETag(Entity Tag) kullanarak sunucu ile istemci arasındaki veri trafiğini nasıl azaltabileceğimizi incelemeye çalışacağız. Öncelikle istemci ile servis arasındaki iletişimi düşünerek ilerlemeye çalışalım. İstemci, sunucu üzerinde yer alan bir operasyon için talepte bulunduğunda bir cevap üretilecek ve buna bağlı bir içerik verisi istemci tarafına indirilecektir. Bu süreç tipik olarak Request-Response senaryosundan farklı bir işleyiş değildir. İstemci sonraki bir zaman diliminde aynı operasyona yeni bir talepte bulunduğunda ise, üretilecek olan sunucu cevabının(Response) bir öncekine göre hiç değişmemiş olma ihtimalide bulunmaktadır. Eğer istemci tarafı bir şekilde gönderdiği talebin karşılığı olan cevabın değişmediğini anlayabilirse ve kendisinde bu içerik zaten tampon alanda duruyorsa, aynı içeriğin sunucudan istemci tarafına bir kere daha indirilmesine gerek yoktur. İşte ETag takısının devreye girdiği nokta burasıdır.

Peki ETag(Entity Tag) tam olarak nedir? Entity Tag, sunucudan istemci tarafına gönderilen Response paketlerinin Header kısmında kullanılabilen bir takıdır. Bu takı yardımıyla bilginin değişikliğe uğrayıp uğramadığı kolayca anlaşılabilir. Bu ayrım bize performans açısından bir kazanım sağlayabilir. Öyleki, sunucu aynı ETag verisine sahip iki Response ürettiğinde, aslında istemcinin talebinin karşılığının bir öncekisi ile aynı olduğu sonucuna varılabilir. Bu noktada istemcinin içeriği tampon bir bölgede tuttuğu düşünüldüğünde, bir önceki ile aynı olan veri içeriğini sunucudan indirmesine gerek kalmayacaktır. Böyle bir durumda sunucun istemci tarafına HTTP 304 Not Modified bilgisi göndermesi söz konusudur. Tabi burada ETag içerisine yazılacak verinin nasıl üretileceği de önemlidir. Genellikle Entity ile alakalı olaraktan son güncelleme zamanı veya checksum kullanılabilir. Hatta SQL veritabanında kullanılabilen Timestamp tipide ETag verisi olarak ciddi anlamda düşünülebilir. Sonuç itibariyle ETag kavramının Caching modeli için çok önemli bir kriter olduğunu söyleyebiliriz.

Şimdi konuyu aşağıdaki içeriğe sahip bir WebHttp Service örneği üzerinden değerlendirmeye çalışalım.

using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;

namespace Lesson2
{
    [ServiceContract]
    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
    public class ProductService
    {
        static List<Product> products = new List<Product>
        {
            new Product{ ProductId="1",Name="Zedojen 500 Gb HDD", Version=Guid.NewGuid()},
            new Product{ ProductId="2",Name="Zedojen 750 Gb HDD", Version=Guid.NewGuid()},
            new Product{ ProductId="3",Name="Leveno Laptop XK 5301", Version=Guid.NewGuid()}
        };

        [WebGet(UriTemplate = "Products/{productId}")]
        public Product GetProduct(string productId)
        {
            var product = (from p in products
                           where p.ProductId == productId
                           select p).FirstOrDefault();
            // Eğer bir Product nesne örneği mevcutsa...
            if (product != null)
            {
                // İstemciden gelen paket Header' ındaki If-None-Match değeri alınır ve sunucu tarafında bulunan Product nesne örneğinin Version özelliğinin değeri ile kıyaslanır. Eğer aynı ise bu istemci tarafına HTTP 304 Not Modified döndürüleceği anlamına gelir.
                WebOperationContext.Current.IncomingRequest.CheckConditionalRetrieve(product.Version);
                // Response içerisinde yer alan HTTP Header içerisindeki ETag değeri sunucu tarafından bulunan Product nesne örneğinin Version özelliği ile set edilir.
                WebOperationContext.Current.OutgoingResponse.SetETag(product.Version);
            }

            return product;
        }               
    }

    public class Product
    {
        public string ProductId { get; set; }
        public string Name { get; set; }
        public Guid Version { get; set; }
    }
}

Servisimizde yer alan GetProduct isimli operasyon geriye Product tipinden bir nesne içeriği döndürmektedir. Product tipinin en önemli özelliklerinden birisi ise Guid tipinden olan Version' dur. Burada veritabanında yer alan bir ürünün ETag için kullanılabilecek veri tipi simüle edilmeye çalışılmaktadır. Yazımızın başında da belirttiğimiz gibi tablo bazlı kaynağın söz konusu olması halinde, Guid yerine Timestamp tipinden bir alan da tercih edilebilir.

GetProduct metodu içerisinde Exception kontrolü yapılmamaktadır. Daha çok üzerinde durmak istediğimiz nokta ETag veri kontrolü ve üretimidir. Bu nedenle WebOperationContext.Current üzerinden çağırılan CheckConditionalRetrieve ve SetETag metodlarına konsantre olmamızda yarar vardır. CheckConditionalRetrieve metodu istemciden gelen talebe ait içerikteki If-None-Match değeri kontrolünü yapmaktadır. Eğer istemci aynı içeriği bir kere daha talep etmişse, bu durumda istediği Product tipinin Version değeri ile gönderdiği If-None-Match değeri aynı olmalıdır. Tabiki buradaki örnek senaryomuzda şu an için veri tarafında yer alan Version alanının değişmediğini düşünüyoruz. Özellikle tarih bazlı olarak tutulan veri içeriklerinde ve örneğin son güncelleme tarihinin ETag olarak kullanıldığı durumlarda ya da herhangibir değişiklik sonrası ilgili versiyon kontrolü alanlarının değerlerinin değiştirildiği hallerde, sunucu tarafından istemciye doğru veri indirilmesi(Download) işlemi yinelenecektir. SetETag metodu ise ilk gelen talep sonrası veya içeriğin istemciye indirilmesi gerektiği talep sonrası, Response' a ait içeriğe o anki Product nesne örneğinin Version değerini atayacaktır. Her iki metodunda farklı tipte aşırı yüklenmiş(Overload) versiyonları bulunmaktadır. Bu versiyonlardan birisi de örneğimizde ele aldığımız Guid veri tipi ile çalışanıdır. Dilerseniz durumu daha iyi anlamak için hemen testlerimize başlayalım. Örneğimizi tarayıcı uygulama üzerinden talep ettiğimizde ilk etapta aşağıdaki örnek sonuçlar ile karşılaşırız.

Not : Burada ipv4.fiddler:1000 şeklindeki kök adres kullanımı mutlaka dikkatinizi çekmiştir. Bunu Fiddler üzerinden örneğimize ait HTTP paketlerini debug edebilmek için kullandığımızı belirtmek isterim.

Örnekte ProductId değeri 1 olan ürüne ait bilgilerin elde edildiği görülmektedir. Bundan sonra 1 numaralı ürünü tekrardan talep edecek olursak Fiddler tarafından aşağıdaki HTTP hareketliliklerinin yakalandığını görebiliriz.

Dikkatinizi çeken bir şey var mı? Wink İlk talep sonrasında sunucudan istemiye HTTP 200 Ok bilgisi dönmüş ve 237 Byte' lık bir Body içeriği indirilmiştir. Diğer yandan aynı talebin ikinci kez yapılması sonrasında istemci tarafına HTTP 304 Not Modified mesajının döndürüldüğü görülmektedir. Üstelik ikinci talep sonrası Body içeriği 0 byte uzunluğundadır. Volaaa!!! Laughing Yani ikinci talebin ilki ile aynı veri üretimine sahip olduğu anlaşılmış ve bu sebepten üretilen paketin istemci tarafına yeniden indirilmesine gerek kalınmamıştır. Örneğimize göre byte seviyesinde bu çok önemli bir performans kazanımına neden olmamaktadır. Ne varki video, resim, müzik gibi büyük boyutlu binary içeriklerin yer aldığı paketlerde 304 döndürülmesinin büyük önemi vardır. Nitekim az önce üretilip istemci tarafına indirilen büyük boyutlu içeriğin, ikinci talep sonrası zaten istemci tarafındaki tamponda duran versiyonu ile aynı olması nedeniyle, yeniden gönderilmesi durumu ortadan kaldırılmakta ve böylece istemci ile sunucu arasındaki ağ trafiğinden akan veri boyutu minimize edilmektedir.

Şimdi Fiddler aracı yardımıyla paketlerin içeriklerine biraz daha yakından bakalım. İlk talep sonrası istemcinin gönderdiği içerik aşağıdaki gibidir.

GET http://127.0.0.1:1000/Adventures/Products/1 HTTP/1.1
Accept: application/x-ms-application, image/jpeg, application/xaml+xml, image/gif, image/pjpeg, application/x-ms-xbap, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, application/x-shockwave-flash, */*
Accept-Language: tr
User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; InfoPath.2; OfficeLiveConnector.1.4; OfficeLivePatch.1.3; .NET4.0C; .NET4.0E; MS-RTC LM 8)
Accept-Encoding: gzip, deflate
Connection: Keep-Alive
Host: 127.0.0.1:1000

Bu talebe karşılık sunucunu cevabı ise aşağıdaki gibi olacaktır.

HTTP/1.1 200 OK
Server: ASP.NET Development Server/10.0.0.0
Date: Fri, 26 Feb 2010 09:43:00 GMT
X-AspNet-Version: 4.0.30128
Content-Length: 237
ETag: "2c1cd636-3058-4985-8f10-3d3cb8c9e5fa"
Cache-Control: private
Content-Type: application/xml; charset=utf-8
Connection: Close

<Product xmlns="http://schemas.datacontract.org/2004/07/Lesson2" xmlns:i="http://www.w3.org/2001/XMLSchema-instance"><Name>Zedojen 500 Gb HDD</Name><ProductId>1</ProductId><Version>2c1cd636-3058-4985-8f10-3d3cb8c9e5fa</Version></Product>

Dikkat edileceği üzere Response içerisinde bir ETag değeri olduğu görülmektedir. Sizce bu değerin 1 numaralı Product' ın güncel Version değeri ile aynı olması bir tesadüf müdür? Wink Ayrıca içeriğin uzunluğu 237 byte' tır.

Gelelim ikinci talebe. İstemci tarafından sunucuya gönderilen ikinci talebin içeriği aşağıdaki gibidir.

GET http://127.0.0.1:1000/Adventures/Products/1 HTTP/1.1
Accept: application/x-ms-application, image/jpeg, application/xaml+xml, image/gif, image/pjpeg, application/x-ms-xbap, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, application/x-shockwave-flash, */*
Accept-Language: tr
User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; InfoPath.2; OfficeLiveConnector.1.4; OfficeLivePatch.1.3; .NET4.0C; .NET4.0E; MS-RTC LM 8)
Accept-Encoding: gzip, deflate
If-None-Match: "2c1cd636-3058-4985-8f10-3d3cb8c9e5fa"
Host: 127.0.0.1:1000
Connection: Keep-Alive

Dikkat edileceği üzere Guid değerine sahip olan If-None-Match isimli element bulunmaktadır. Bu değer biraz önceki talep sonucu istemciye gönderilen ETag değeridir aslında. Şimdi bu noktada sunucu üzerinde yer alan 1 numaralı ürünün içeriğinin değiştirilmediği ve bu nedenle Guid değerinin de aynı olduğu düşünülmektedir. Bu sebepten sunucu tarafından istemciye gönderilen cevabın içeriği aşağıdaki gibi olacaktır.

HTTP/1.1 304 Not Modified
Server: ASP.NET Development Server/10.0.0.0
Date: Fri, 26 Feb 2010 09:43:05 GMT
X-AspNet-Version: 4.0.30128
ETag: "2c1cd636-3058-4985-8f10-3d3cb8c9e5fa"
Cache-Control: private
Connection: Close

Her hangibir içerik(Content) yoktur. Hatta 0 byte uzunluğunda bir Content mevcuttur. Ama daha önemlisi yine ETag elementi vardır ve Guid değerini içermektedir. Ayrıca HTTP 304 Not Modified bilgisinin döndürüldüğü dikkatlerden kaçmamalıdır.

Peki sunucu tarafındaki Product içeriğinde ve dolayısıyla Version değerinde bir değişme olursa? Bir veritabanı örneği geliştirmediğimiz için bu durumu şu şekilde simüle edebiliriz
; 1 numaralı ProductId değerine sahip ürünün adını Visual Studio 2010 ortamında değiştirip örneği tekrardan build ederek. Yeniden build işlemi sonucu static olarak tanımlanan List<Product> koleksiyon içeriğinin üretimi yinelenecektir. Bu da yeni Guid değerlerinin üretimi anlamına gelmektedir. Bu durumda servis operasyonuna yeniden talepte bulunursak aşağıdaki cevabı aldığımız görürüz.

HTTP/1.1 200 OK
Server: ASP.NET Development Server/10.0.0.0
Date: Fri, 26 Feb 2010 11:52:38 GMT
X-AspNet-Version: 4.0.30128
Content-Length: 237
ETag: "2fadf1be-d5c3-4fe0-a9c6-ecf20437ffe4"
Cache-Control: private
Content-Type: application/xml; charset=utf-8
Connection: Close

<Product xmlns="http://schemas.datacontract.org/2004/07/Lesson2" xmlns:i="http://www.w3.org/2001/XMLSchema-instance"><Name>Zedojen 250 Gb HDD</Name><ProductId>1</ProductId><Version>2fadf1be-d5c3-4fe0-a9c6-ecf20437ffe4</Version></Product>

Dikkat edileceği üzere Guid değeri bir öncekinden farklıdır ve istemciye HTTP 200 Ok koduyla Product içeriği tekrardan gönderilmiştir. Tabi bunun sonrasında 1 numaralı ürünü yeninden talep edersek yine HTTP 304 Not Modified durumu ile karşılaşırız.

Buraya kadar her şey iyi gitti. Ancak testlerimizi farkettiğiniz üzere Internet Explorer gibi tarayıcı uygulamalar üzerinden gerçekleştirdik. Oysaki istemci uygulamayı biz yazıyorsa ETag kullanımı için de yapmamız gereken ekstra işlemler söz konusudur. Bu amaçla az önce geliştirdiğimiz servis uygulamasını test edeceğimiz basit bir Console Application geliştirdiğimizi düşünelim. Kod içeriğini aşağıdaki gibi yazmamız ETag desteği için yeterli olacaktır.

using System;
using Microsoft.Http;
using Microsoft.Http.Headers;

namespace Client
{
    class Program
    {
        static void Main(string[] args)
        {
            Uri serviceAddress = new Uri(@"http://ipv4.fiddler:1000/Adventures/");
            using (HttpClient client = new HttpClient(serviceAddress))
            {
                EntityTag eTag=null;
                Process(client, "1",ref eTag);
                Process(client, "1",ref eTag);
            }
        }

        static void Process(HttpClient client, string ProductId,ref EntityTag ETag)
        {
            Console.WriteLine("***{0} için Talep***\n",ProductId);
            // Talebin hazırlanması ve gönderilmesi
            using (HttpRequestMessage request = new HttpRequestMessage("GET", "Products/"+ProductId))
            {
                // Metoda referans olarak gelen EntityTag tipinden olan ETag değerine bakılır. Eğer null değil ise ki ilk talep sonrası sunucu tarafından ürünün Version değeri ile doldurulacaktır; bu durumda Header kısmına If-None-Match değerinin eklenmesi sağlanır.
                if (ETag != null)
                    request.Headers.IfNoneMatch.Add(ETag);

                // If-None-Match değeri içeren talep gönderilir
                using (HttpResponseMessage response = client.Send(request))
                {
                    // ETag değeri gelen cevaptan alınır ve ref tipinden olan metod parametresine aktarılır. Böylece Process metoduna yapılacak olan sonraki çağrılarda aynı ETag değerinin taşınması kolaylaşmaktadır.
                    ETag = response.Headers.ETag;
                    // Sonuçlar ekran yazdırılır.
                    Console.WriteLine("StatusCode : {0}\n", response.StatusCode);
                    Console.WriteLine("Content : {0}\n",response.Content.ReadAsString());
                    Console.WriteLine("ETag Değeri : {0}\n", ETag.Tag);
                }
            }
            Console.WriteLine("*******");
        }
    }
}

Uygulamamızı çalıştırdığımızd aşağıdaki sonuçlar ile karşılaşırız.

Görüldüğü gibi, ikinci talep sonrasında istemci tarafına HTTP 304 Not Modified bilgisi ve 0 Byte uzunluğunda içerik gönderilmiştir. Böylece WCF WebHttp Service' leri ile ilişkili bir yazımızın daha sonuna geldik. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Lesson9_RC.rar (175,04 kb) [Örnek Visual Studio 2010 Ultimate RC sürümü üzerinde geliştirilmiş ve test edilmiştir]

WCF WebHttp Services - Özel Formatta Mesaj Döndürmek

Perşembe, 8 Nisan 2010 16:39 by bsenyurt

Merhaba Arkadaşlar,

Bu yazımızda bizleri uzun, zorlu ve yorucu bir macera bekliyor. Şimdiden söylemek isterim ki yanınızda tatlı(Mesela kolalı jelibon olabilir), tuzlu yiyecek bir şeyler, boğaz kuruluğunuzu giderecek içecekler veya daha fazla oksijen çekmenizi sağlayacak sakızlar olsun. Unutmadan birde aspirin. Baş ağrısı için Laughing  Gelelim bu günkü konumuza.

Bu yazımızda, son günlerde sıklıkla üzerinde durduğumuz WCF WebHttp Service' lerinde, istemciden gelen root adres bazlı taleplerin nasıl karşılanacağını ve özel formatta mesajların nasıl döndürüleceğini incelemeye çalışıyor olacağız. Ancak işe başlamadan önce ihtiyacın ne olduğundan bahsetmemizde yarar var. Bu amaçla bir Web uygulaması üzerinden host edilen birden fazla WebHttp servisimiz olduğunu düşünerek ilerleyelim. Bu servisler içerisinde de örneğin HTTP Get taleplerinin karışılığında çeşitli tipte koleksiyonları döndüren operasyonlarımız olduğunu farz edelim. Bu durumda global.asax dosyasındaki kodlarda yönlendirme tablosuna ekleyeceğimiz adres bilgilerine göre, gelen talepleri uygun olan servislere yöndermemiz mümkün olacaktır. Bunu zaten daha önceki bir yazımızda incelemiştik.

Söz gelimi http://makineadı:port numarası/CompanyServices/AdventureWorks/Products ile http://makineadı:port numarası/CompanyServices/Chinook/Albums gibi iki talep gönderildiğini düşünelim. Bu taleplerin aynı web uygulamasından host edilen iki farklı servis tipi tarafından değerlendirildiği bir durumda, doğru yönlendirme tekniği ile uygun olan servis ve operasyonunun çağırılması mümkündür. Oysaki istemciler http://makineadı:port/CompanyServices/ adresine de talepte bulunulabilir. Böyle bir durumda ne olur? Gelin bunu açıklamak için aşağıdaki örnek servis sınıflarını içeren bir WCF REST Service Application projemiz olduğunu düşünelim.

AdventureWorksService sınıfı;

using System.Collections.Generic;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;

namespace Lesson8

    [ServiceContract]
    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
    public class AdventureWorksService
    {
        [WebGet(UriTemplate = "Products")]
        public List<Product> GetProducts()
        {
            return new List<Product>
            {
                new Product{ Id=1, Name="Büyüteç", ListPrice=1.24M},
                new Product{ Id=2, Name="Stabilo Pen 68", ListPrice=2.35M},
                new Product{ Id=3, Name="Temizleme Spreyi", ListPrice=4.19M}
            };
        }
    }
}

Söz konusu sınıf içerisinde yer alan GetProducts isimli operasyon geriye Product tipinden bir ürün listesi döndürmekle görevlendirilmiştir. Tahmin edeceğiniz üzere şu anda kendi kendimizi yetiştirmeye çalıştığımızdan sadece anlamsız bir liste üretimi söz konusudur.

ChinookService sınıfı;

using System.Collections.Generic;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;

namespace Lesson8

    [ServiceContract]
    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
    public class ChinookService
    {
        [WebGet(UriTemplate = "Artists")]
        public List<Artist> GetArtists()
        {
            return new List<Artist>
            {
                new Artist{ Id=1, Name="Ayrosimit",IsGroup=true},
                new Artist{ Id=2, Name="Megadet",IsGroup=true},
                new Artist{ Id=3, Name="Metalika",IsGroup=true},
                new Artist{ Id=4, Name="Co Satriani",IsGroup=false}
            };
        }
    }
}

ChinookService sınıfıda, AdventureWorksService tipine benzer bir şekilde ama bu kez Artist tipinden liste döndüren tek bir operasyon içermektedir. Buraya kadar zaten bir sorun bulunmamaktadır. Ancak aynı web uygulamasında birden fazla servisi host etmek istediğimizde, RouteTable nesnesinin Routes koleksiyonu içerisinde gerekli düzenlemelerin de yapılması gerekmektedir. Bu nedenle global.asax.cs içeriğinin aşağıdaki gibi olduğunu düşünebiliriz.

using System;
using System.ServiceModel.Activation;
using System.Web;
using System.Web.Routing;

namespace Lesson8
{
    public class Global
        : HttpApplication
    {
        void Application_Start(object sender, EventArgs e)
        {
            RegisterRoutes();
        }

        private void RegisterRoutes()
        {
            RouteTable.Routes.Add(new ServiceRoute("AdventureWorks", new WebServiceHostFactory(), typeof(AdventureWorksService)));
            RouteTable.Routes.Add(new ServiceRoute("Chinook", new WebServiceHostFactory(), typeof(ChinookService)));
        }
    }
}

Buna göre istemciden gelecek olan http://localhost:10843/CompanyServices/AdventureWorks/Products ve http://localhost:10843/CompanyServices/Chinook/Artists talepleri sorunsuz bir şekilde karşılanacaktır. Ancak doğrudan Web uygulamasının Root adresine yapılan http://localhost:10843/CompanyServices/ gibi bir talepte aşağıdaki ekran görüntüsü ile karşılaşılacaktır.

Elbette Help sayfalarına gidilerek servislere nasıl talepte bulunulabileceği öğrenilebilir. Ancak elimizde iki servis olduğundan söz konusu yardım sayfalarına gitmek için http://localhost:10843/CompanyServices/AdventureWorks/help veya http://localhost:10843/CompanyServices/Chinook/help gibi taleplerinin gönderilmesi gerekmektedir.

Sanıyorum ki nihayet ne yapmak istediğimize gelebildik. İstediğimiz şey http://localhost:10843/CompanyServices/ adresine yapılan talep ile Web uygulamasından sunulan servisleri bildirmek olacak. Üstelik bu talebe karşılık dönecek mesajın içeriğini kendimiz tasarlayacağız. Yapabilir miyiz? Evet yapabiliriz. Çünkü gerekli tüm tipler Framework içerisinde çoktandır mevcutlar. Özetle talebe uygun bir formatta(XML, JSON, ATOM gibi) kendi veri yayınımızı yapacağımızı ifade edebiliriz.

İşe ilk olarak yeni bir servis sınıfını geliştirerek başlamamız gerekiyor. Bu sınıf içerisinde yer alan operasyonumuz geriye, System.ServiceModel.Channels isim alanında yer alan Message tipinden bir değer döndürüyor olacak. İşte EntranceService isimli yeni sınıfımızın içeriği;

using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Channels;
using System.ServiceModel.Syndication;

using System.ServiceModel.Web;

namespace Lesson8

    [ServiceContract]
    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
    public class EntranceService
    {
        // İstemci tarafına sunulacak olan kaynakların listesi Resource tipinden generic bir koleksiyonda tutulur.
        public static readonly List<Resource> Resources = new List<Resource>
        {
            new Resource{ Name="Chinook List", Description="Chinook nesneleri listesi", RequestUri=new Uri("Chinook",UriKind.Relative),HelpUri=new Uri("Chinook/help",UriKind.Relative)},
            new Resource{ Name="Adventure Works List", Description="Adventure Works nesneleri listesi", RequestUri=new Uri("AdventureWorks",UriKind.Relative),HelpUri=new Uri("AdventureWorks/help",UriKind.Relative)}
        };

        [WebGet(UriTemplate="")]
        public Message GetResources()
        {
            WebOperationContext optContext = WebOperationContext.Current;
            IncomingWebRequestContext incomingRequest = optContext.IncomingRequest;

            string mType=string.Empty;
            foreach (var acceptType in incomingRequest.GetAcceptHeaderElements())
            {
                // Gelen talebin Header bilgisinde yer alan MediaType değerine göre geriye Xml, Json veya Atom formatında bir içerik döndürülmesi sağlanır.
                 mType= acceptType.MediaType.ToLower();

                 if (mType == "application/xml" || mType == "text/xml")
                     return optContext.CreateXmlResponse(Resources); // Xml formatında dönüş
                 else if (mType == "application/json")
                     return optContext.CreateJsonResponse(Resources);
                 else if (mType == "application/atom+xml") // Json formatında dönüş
                 {
                     // Atom formatında dönüş için SyndicationFeed nesnesinin örneklenmesi gerekmektedir. Resource değişkeninin işaret ettiği koleksiyon bu nesne örneği içerisindeki Items koleksiyonuna atanır
                     return optContext.CreateAtom10Response(
                         new SyndicationFeed(
                             "Company Services Resources",
                             "Adventure Works & Chinook Kaynakları",
                             new Uri("", UriKind.Relative),
                             Resources.Select(r => new SyndicationItem(r.Name, r.Description, r.RequestUri)
                             )));
                 }
            }
            // Varsayılan olarak çıktı XML formatında verilir
            return optContext.CreateXmlResponse(Resources);
        }
    }

    // Servisten sunulan servislerin birer kaynak olduğu düşünüldüğünde bu servislere ait ad, açıklama, root Uri ve help page uri bilgilerinin saklandığı tip
    public class Resource
    {
        public string Name { get; set; }
        public string Description { get; set; }
        public Uri RequestUri { get; set; }
        public Uri HelpUri { get; set; }
    }
}

GetResources isimli servis operasyonunun en önemli özelliği WebGet niteliğinde boş bir template kullanılması ve tabiki geriye Message tipinden bir değer döndürmesidir. Servis operasyonu dikkatlice incelendiğinde, istemciden gelecek olan taleplere ait Header kısımlarında yer alan mesaj formatı bilgisine göre bir çıktı üretildiği görülebilir. Buna göre standart olarak XML, JSON ve ATOM formatlarında bir üretim söz konusudur. ATOM formatındaki çıktının hazırlanması sırasında bir SyndicationFeed nesnesinin örneklendiğine dikkat edilmelidir. Tüm bu formatlama işlemlerinde o anki Web içeriği referansını taşıyan WebOperationContext tipine ait Create... metodlarından yararlanılmaktadır.

Tabi işimiz bu servis sınıfını yazmakla bitmiş değil. Fark ettiğiniz üzere üçüncü bir servis sınıfımız oldu ve bu sınıfa gelen talepler UriTemplate bilgisine göre Web uygulamasının Root adresine yapılmakta. Dolayısıyla boş template için gerekli yönlendirme bilgisinin global.asax.cs içerisinde bildirilmesi gerekiyor. Aynen aşağıda olduğu gibi.

using System;
using System.ServiceModel.Activation;
using System.Web;
using System.Web.Routing;

namespace Lesson8
{
    public class Global
        : HttpApplication
    {
        void Application_Start(object sender, EventArgs e)
        {
            RegisterRoutes();
        }

        private void RegisterRoutes()
        {
            RouteTable.Routes.Add(new ServiceRoute("", new WebServiceHostFactory(), typeof(EntranceService)));
            RouteTable.Routes.Add(new ServiceRoute("AdventureWorks", new WebServiceHostFactory(), typeof(AdventureWorksService)));
            RouteTable.Routes.Add(new ServiceRoute("Chinook", new WebServiceHostFactory(), typeof(ChinookService)));
        }
    }
}

Buna göre örnek bir tarayıcı uygulama üzerinden http://localhost:10843/CompanyServices/ adresine yapacağımız bir talebin sonucu aşağıdaki gibi olacaktır.

Her şey yolunda görünüyor. Ancak ufak bir pürüz var. EntranceService tipine boş Uri bilgisi üzerinden bir başka deyişle Web uygulamasına ait Root adresten gidilebildiği için, http://localhost:10843/CompanyServices/help şeklinde gönderilen bir talepte aşağıdaki ekran görüntüsü ile karşılaşılacaktır.

Oysaki bu Help sayfasının çıkmasına pekte gerek yoktur. Bu bir zorunluluk değildir ama olmasının da bir anlamı yoktur. Dolayısıyla pasif hale getirmemiz gerekmektedir. Bu amaçla WCF 4.0 ile birlikte konfigurasyon dosyasına getirilen yeniliklerden yararlanarak gerekli sonucu elde edebiliriz. Tek yapmamız gereken Web.config dosyasını aşağıdaki gibi düzenlemek.

<?xml version="1.0"?>
<configuration>
 
  <system.web>
    <compilation debug="true" targetFramework="4.0" />
  </system.web>

  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true">
      <add name="UrlRoutingModule" type="System.Web.Routing.UrlRoutingModule, System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
    </modules>
  </system.webServer>

  <system.serviceModel>
    <serviceHostingEnvironment aspNetCompatibilityEnabled="true"/>
    <standardEndpoints>
      <webHttpEndpoint>
        <!-- Diğer servislerin help sayfalarının disable olmaması için ilk standartEndpoint elementine dokunulmamalıdır-->
        <standardEndpoint name="" helpEnabled="true" automaticFormatSelectionEnabled="true"/>
        <standardEndpoint name="EntranceServiceEndPoint"/><!-- Varsayılan olarak helpEnabled özelliği false değere sahiptir-->
      </webHttpEndpoint>
    </standardEndpoints>
    <services>
      <service name="Lesson8.EntranceService">       
        <endpoint contract="Lesson8.EntranceService" kind="webHttpEndpoint" endpointConfiguration="EntranceServiceEndPoint"/>
      </service>
    </services>
  </system.serviceModel>

</configuration>

Zaten varsayılan web.config dosyası içeriğine göre, tüm servis talepleri otomatik olarak standart bir Endpoint tipine yönlendirilir. Ancak senaryomuza göre ana adres üzerinden yapılan yardım sayfası talebi geçersiz olmalı, diğerleri ile kullanılabilir durumda kalmalıdır. Bu nedenle EntranceService isimli hizmet için de bir Endpoint tanımlaması yapılmış ve webHttpEndpoint içerisindeki farklı bir ayara yönlendirilmiştir. Buradaki düzenlemeye göre EntranceService dışındaki tüm servislerin help sayfalarına ulaşılabilmektedir. Ancak EntranceService için help sayfası gösterilmemektedir. Buna göre http://localhost:10843/CompanyServices/help adresine bir talepte bulunulduğunda aşağıdaki ekran görüntüsü ile karşılaşılacaktır.

Servis tarafında pek çok işimizi hallettik. Ancak test etmemiz gereken bir husus daha var. İstemcinin, XML dışında ATOM veya JSON formatlı mesaj taleplerine karşılık olarak nasıl sonuçlar alacağı. Nitekim tarayıcı üzerinden yaptığımız taleplerde standart olarak XML çıktısı aldığımızı gördük ve biliyoruz. Peki ya diğer formatlar? Bu durumu test etmek için yine basit bir Console uygulaması geliştiriyor olacağız. Her zamanki gibi HttpClient tipinden yararlanacağız. Bu sebepten REST Starter Kit ile gelen Microsoft.Http ve Microsoft.Http.Extensions Assembly referanslarını eklemeyi unutmayalım. İşte Console uygulamamıza ait kodlarımız.

using System;
using Microsoft.Http;

namespace ClientApp
{
    class Program
    {
        static void Main(string[] args)
        {
            using (HttpClient client = new HttpClient("http://localhost:10843/CompanyServices/"))
            {
                Execute(client,"application/json"); // Json formatında talep gönderilir
                Execute(client, "application/atom+xml"); // atom formatında talep gönderilir
                Execute(client, "noFormat"); // Olmayan bir format için talep gönderilir
            }
        }

        private static void Execute(HttpClient client,string acceptFormat)
        {
            // HttpRequestMessage nesnesi örneklenirken kullanılan ikinci parametreye göre EntranceService tarafından karşılanacak bir talep oluşturulur
            using (HttpRequestMessage requestMessage = new HttpRequestMessage("GET", String.Empty))
            {
                // Accept özelliğinin Add metodu yardımıyla Header' a eklenen bilgiye göre hangi formatta mesaj istendiğin belirtilir.
                requestMessage.Headers.Accept.Add(acceptFormat);
                using (HttpResponseMessage responseMesssage = client.Send(requestMessage))
                {
                    Console.WriteLine("\n{0}\n", responseMesssage.Content.ReadAsString());
                }
            }
        }
    }
}

Kodlarımızı çalıştırdığımızda aşağıdaki ekran görüntüsünde yer alan sonuçları elde ederiz.

Vuuuvvvv!!! Şu anda masamdaki şekerlerin oranına bakıyorum da...Baya bir tüketmişim. Sealed Artık dinlenmeye çekilmenin vakti geldi sanırım. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Lesson8_RC.rar (181,52 kb) [Örnek Visual Studio 2010 Ultimate Beta 2 Sürümünde geliştirilmiş ancak RC sürümü üzerinde de test edilmiştir]

WCF WebHttp Services - Client Bazlı Cache

Salı, 30 Mart 2010 16:30 by bsenyurt

Merhaba Arkadaşlar,

Bir önceki yazımızda(WCF WebHttp Services - Server Bazlı Cache) hatırlayacağınız üzere WCF WebHttp Service' lerinde sunucu taraflı ön belleklemeyi(Server-Based Caching) incelemeye çalışmış ve bu işin birde istemci taraflı olanından bahsetmiştik. Aslında sunucu ve istemci taraflı ön bellekleme işleyişleri birbirlerinden tamamen farklıdır. Sunucu taraflı ön bellekleme işleyişinde, tamponlanan veriyi üreten operasyonun duration süresi dolana kadar çalıştırılmaması söz konusudur. Yani istemciden gelen ilk talebin sonucunun ön belleğe alınmasını takiben gelen taleplerde, sunucu tarafındaki operasyon kodları icra edilmemektedir. Ne varki istemci taraflı ön belleklemenin işleyişine göre sunucu tarafındaki kodlar icra edilir ve ön bellekleme yapılacağı, HTTP Cache-Control bilgisinin istemciye gönderilen cevabın(Response) Header kısmına eklenmesi ile anlaşılır. Bir başka deyişle ilk talepten sonra gelecek taleplerde yine servis kodunun çalıştırılması gündemdedir. Dolayısıyla ispatı ve analizi pekte kolay olmayan bir konu ile karşı karşıyayız. Bu yüzden en azından nasıl hayata geçirilebileceğini görmeye çalışacağız. Elbette bir örnek geliştirerek. Gelin tembellik etmeyerek bir önceki uygulamamızdan devam etmek yerine yeni bir örnek üzerinden istemci taraflı ön belleklemenin nasıl yapılacağını araştıralım. Öncelikle aşağıdaki servis kodlarına sahip olan bir WCF REST Service Application projemiz olduğunu düşünelim.

using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;
using System.Web;

namespace Lesson7
{
    [ServiceContract]
    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
    public class FunnyService
    {
        List<Wallpaper> wallpapers = new List<Wallpaper>
            {
                new Wallpaper{ Id=1000, Name="Ahhh!", Image=File.ReadAllBytes(HttpContext.Current.Server.MapPath("~/Images/Ahhh.jpg"))},
                new Wallpaper{ Id=1001, Name="Blue Light", Image=File.ReadAllBytes(HttpContext.Current.Server.MapPath("~/Images/BlueLight.jpg"))},
                new Wallpaper{ Id=1002, Name="My Dear Manager", Image=File.ReadAllBytes(HttpContext.Current.Server.MapPath("~/Images/Manager.jpg"))},
                new Wallpaper{ Id=1003, Name="No Sacrifice No Victory", Image=File.ReadAllBytes(HttpContext.Current.Server.MapPath("~/Images/Sacrifice.jpg"))}
            };
               
        [AspNetCacheProfile("WallpaperCache")] //web.config dosyasında WallpaperCache ismi ile output cache konfigurasyonu yapılmaktadır.
        [WebGet(UriTemplate = "Wallpapers/{Name}")]
        public List<Wallpaper> GetWallpapers(string Name)
        {
            var result = (from w in wallpapers
                         where w.Name.ToLower().Contains(Name.ToLower())
                         select w).ToList();
            return result;
        }
    }

    public class Wallpaper
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public byte[] Image{ get; set; }
    }
}

GetWallpapers isimli operasyonun çalıştırılması sonucunda istemci tarafına, Images klasörü altında yer alan jpg uzantılı resim dosyalarına ait byte[] içeriklerini taşıyan ve Wallpaper tipinden nesne örneklerinden oluşan generic List<T> koleksiyonu döndürülmektedir(Elbette varsayılan mesaj formatına göre XML olarak). AspNetCacheProfile niteliği yardımıyla söz konusu operasyon için Caching işleminin icra edileceği bildirilir. Bu ön belleklemenin hangi kriterlere göre yapılacağına ait tanımlamalar ise web.config dosyası içerisinde aşağıdaki gibi belirlenir.

<?xml version="1.0"?>
<configuration>
 
  <system.web>
    <compilation debug="true" targetFramework="4.0" />
    <caching>
      <outputCache enableOutputCache="true"/>
      <outputCacheSettings>
        <outputCacheProfiles>
          <!-- İstemci taraflı ön bellekleme query string parametresi veya Header kısımlarını dikkate almaz. Bu nedenle varyByParam niteliğine none değerini atanmış ve varyByHeader niteliği kullanılmamıştır. Dikkat edileceği üzere ön belleklemenin istemci taraflı olacağı location niteliği ile belirlenmiştir.-->
          <add name="WallpaperCache" duration="120" varyByParam="none" location="Client"/>
        </outputCacheProfiles>
      </outputCacheSettings>
    </caching>
  </system.web>

Konfigurasyon dosyasının devamı...

OutputCacheProfiles içerisine eklenen WallpaperCache profiline göre Cache süresi 120 saniyedir. Diğer taraftan istemci taraflı ön bellekleme işlemlerinde Querystring kullanımı bir faktör olmadığından varyByParam niteliğine none değeri atanmalıdır. Bu profilde belkide en önemli nokta location niteliğinin değerinin Client olarak belirlenmesidir. Böylece ön bellekleme işleyişinin istemci taraflı yapılacağı tayin edilimiş olur.

Tabi bu durumu incelemek için istemci tarafının da geliştirilmesi gerekmektedir. Nitekim istemci tarafına gönderilen cevabın Header kısmında, Caching ile ilgili bilgilerin bulunması gerekmektedir. Bu amaçla çok basit olarak aşağıdaki kodlara sahip bir Console uygulaması yazdığımızı düşünebiliriz (HttpClient ve diğer yardımcı tiplerin kullanımı söz konusu olduğundan REST Starter Kit' in parçası olan Microsoft.Http ve Microsoft.Http.Extension assmebly' larının referans edilmesi gerektiğini unutmayalım)

using System;
using Microsoft.Http;

namespace ClientApp
{
    class Program
    {
        static void Main(string[] args)
        {
            // HttpClient nesnesi url adresine göre oluşturulur.
            HttpClient client = new HttpClient("http://localhost:1000/");

            Send(client);

            Console.WriteLine("Kapatmak için bir tuşa basınız");
            Console.ReadLine();

        }
        static void Send(HttpClient client)
        {
            Console.WriteLine("\nAradığınız kelimeyi girin\n");
            string word = Console.ReadLine();
            // Wallpapers için gerekli request string oluşturulur. word bilgisi kullanıcı tarafından girilmektedir.
            string requestString = String.Format("Wallpapers/{0}", word);

            // Get metodundan yararlanılarak sunucu tarafına ilgili talep gönderilir
            using(HttpResponseMessage responseMessage = client.Get(requestString))
               {
   
             // Sunucudan gelen cevap içerisinde yer alan Cache bilgisi ve ayrıca HTTP Statü koduna bakılır
                Console.WriteLine("{0}\n{1}\n",responseMessage.Headers.CacheControl.MaxAge,responseMessage.StatusCode);
           
                // İçerik okunur
               Console.WriteLine(responseMessage.Content.ReadAsString());
            }
        }
    }
}

İstemci uygulamadan kullanıcının girdiği kelimeye göre HTTP Get formatında bir talebinin gönderilmesi sağlanmaktadır. Sunucu tarafından çağırılan operasyona ait ön bellekleme işleminin istemci bazlı olduğu, istemci tarafına gönderilen cevap(Response) içerisinden öğrenilebilir. Bu amaçla örnek olarak HttpResponseMessage nesnesinin Header.CacheControl özelliği üzerinden yakalanan MaxAge değerine bakılmıştır. Bu değer dikkat edileceği üzere sunucu üzerinde belirtilen duration süresinin Timespan karşılığıdır(örneğimize göre 2 dakika).

Tabiki Caching özelliğini sunucu tarafında kapatırsak ki bunun için web.config dosyasında ilgili cache profile' in enabled niteliğine false değeri atamamız yeterlidir,

<add name="WallpaperCache" duration="120" varyByParam="none" location="Client" enabled="false"/>

ve aynı örneği yeniden test edersek aşağıdaki sonuçlar ile karşılaşırız.

çok doğal olarak bir ön bellekleme süresi olmayacaktır. Örneği Fiddler gibi bir araç yardımıyla incelediğimizde ise istemci tarafına dönen cevaba ait Cache bilgisini daha detaylı bir şekilde görebiliriz.

WCF WebHttp Service' ler ile ilişkili serimizde yer alan bir yazımızın daha sonuna geldik. Bu yazımızda istemci bazlı ön bellekleme işlemleri için ne gibi hazırlıklar yapılması gerektiğini gördük. En önemli noktanın web.config dosyası içerisinde belirtilen location niteliği olduğunu özetleyebiliriz. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Lesson7_RC.rar (192,46 kb) [Örnek Visual Studio 2010 Ultimate Beta 2 Sürümünde geliştirilmiş ancak RC sürümü üzerinde de test edilmiştir]

WCF WebHttp Services - Server Bazlı Cache

Salı, 30 Mart 2010 00:05 by bsenyurt

Merhaba Arkadaşlar,

Şirketimi çok seviyorum. Nazar değmesin ama araştırma yapmam, yeni bir şeyler öğrenmem ve bunları ekip arkadaşlarımla paylaşmam için beni özellikle teşvik eden bir şirkette bulunmaktayım. Çalıştığım şirketin en güzel özelliklerinden biriside, Cuma günleri yapılan minik ikramları. Wink Her cuma değişik bir yiyecek ile karşılaşıyoruz. Geçtiğimiz Cuma' lardan birisinde ise yanda çektiğim resimde görülen gülen kurabiyelerimiz vardı. E böylesine güler yüzlü kurabiyeler ile gerekli glikozu aldıktan sonra içimden hemen eve gitmek gelmedi. Bunun yerine mesai sonrasında çalışma masamda oturup, etrafın sakinleşmesi ve sessizliğin artması ile birlikte bloğuma bir şeyler yazmaya karar verdim. Bir süredir WCF Eco System' in parçaları üzerinde yazmakta olduğum bir seri bulunmaktaydı. Bunu devam ettirmek ile Cuma gecesini güzelce tamamlayabileceğimi düşündüm. İşte bu günkü konumuz...WCF WebHttp Service' lerinde ön bellekleme(Output Caching).

WCF WebHttp Service' leri bildiğiniz üzere Web ortamı üzerinden sunulan hizmetlerdir. Bu sebepten Web tarafının sunucu ve istemci bazlı bazı yeteneklerini kullanabilirler. Örneğin Asp.Net Compatibility Mode ile çalıştırıldıklarında Asp.Net dünyasının Output Cache yeteneğine sahip olurlar. Bildiğiniz üzere Output Cache mekanizması sayesinde Web içeriklerine gelen taleplerin ön bellekten karşılanması ve bu sayede arka planda ilgili HTML çıktılarının üretilmesi için gerekli işlemlerin otomatikman atlanılması sağlanabilmektedir. Bu, özellikle üretim maliyeti yüksek olan ama belirli bir süre zarfı içerisinde değişmeyen sayfa içeriklerinin üretiminde oldukça performans arttırıcı bir tekniktir. Madem Web tarafında böyle bir yeteneğimiz bulunmaktadır, o halde neden bu kabiliyeti WCF Servislerinde de kullanamayalım? İşte bu yazımızda Asp.Net tarafında hazır olan bu alt yapının WebHttp Service' lerinde nasıl kullanıldığını incelemeye çalışıyor olacağız. İlk olarak konuyu sunucu bazlı ön bellekleme(Server Side Caching) olarak değerlendireceğiz. Serinin sonraki bölümünde ise istemci taraflı ön belleklemeyi ele alacağız. Dilerseniz vakit kaybetmeden kodlamaya başlayalım. Öncelikli olarak aşağıdaki WebHttp Service içeriğine sahip bir WCF REST Service Application geliştirdiğimizi düşünelim.

using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;
using System.Web;

namespace Lesson6
{  
    [ServiceContract]
    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
    public class EinsteinService
    {
        // GetCategories isimli metodun çıktısının ön bellekleneceği AspNetCacheProfile niteliği ile belirtilir. Nitelikte parametre olarak kullanılan string bilgi ile web.config dosyasında bu metod için nasıl bir ön bellekleme yapılacağı set edilir. Ne kadar süre ile tutulacağı, parametre bazlı olup olmayacağı gibi...
        [AspNetCacheProfile("CategoriesCache")]
        [WebGet(UriTemplate = "Categories")]
        public List<string> GetCategories()
        {           
            string[] categories=File.ReadAllLines(HttpContext.Current.Server.MapPath("~/Kategoriler.txt"));
            return new List<string>(categories);
        }

        [AspNetCacheProfile("CategoriesByFirstLetterCache")]
        [WebGet(UriTemplate = "Categories/{firstLetter}")]
        public List<string> GetCategoriesByFirstLetter(string firstLetter)
        {
            string[] categories = File.ReadAllLines(HttpContext.Current.Server.MapPath("~/Kategoriler.txt"));
            return (from category in categories
                         where category.StartsWith(firstLetter,true,null)
                         select category).ToList();
        }
    }
}

Örneğimizde yer alan servisimizde iki operasyon yer almaktadır. Her iki operasyonda aşağıda örnek çıktısı olan Kategoriler.txt isimli text tabanlı dosyayı kullanmaktadır.

GetCategories operasyonu text dosyası içerisindeki tüm satırları string tipinden generic List koleksiyonu olarak geriye döndürmektedir. GetCategoriesByFirstLetter operasyonu ise aynı çıktıyı baş harflere göre üretmektedir. Bizim odaklanmamız gereken nokta ise her iki operasyon başında uygulanan AspNetCacheProfile niteliğidir(Attribute). Bu niteliklerin uygulanması ile söz konusu operasyonların çıktılarının ön bellekleneceği, çalışma zamanı ortamına bildirilmektedir. Her iki nitelikte birbirlerinden benzersiz olan takma adlar(Alias) ile işaret edilmektedir. Peki bu isimleri nerede değerlendireceğiz? Bu sorunun cevabı Web.config dosyasında yer alan Asp.Net Output Cache ayarlarında gizlidir...Wink Buradaki ayarlar ile hangi operasyon için nasıl bir ön bellekleme işleminin uygulanacağını belirtebiliriz. Örneğin operasyonların sonuçlarının ne kadar süreyle ön bellekte tutulacaklarını farklılaştırabiliriz. Yada parametre bazlı olanları...Örneğin GetCategoriesByFirstLetter operasyonunun çalışma zamanı HTML çıktılarının firstLetter bilgisine göre ön belleklenebileceğini belirtebiliriz. Tüm bu ayarlamalar için web.config dosyasına aşağıdaki eklemeleri yapmamız yeterlidir.

<?xml version="1.0"?>
<configuration>
 
  <system.web>
    <compilation debug="true" targetFramework="4.0" />

    <caching>
      <outputCache enableOutputCache="true"/>
      <outputCacheSettings>
        <outputCacheProfiles>
          <add name="CategoriesCache" duration="120" location="Server" varyByParam="none" varyByHeader="Accept"/>
          <add name="CategoriesByFirstLetterCache" duration="300" location="Server" varyByParam="firstLetter" varyByHeader="Accept"/>
        </outputCacheProfiles>
      </outputCacheSettings>
    </caching>
  </system.web>

  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true">
      <add name="UrlRoutingModule" type="System.Web.Routing.UrlRoutingModule, System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
    </modules>
  </system.webServer>

  <system.serviceModel>
    <!--Asp.Net Output Caching alt yapısını ve yeteneklerini kullanmak istediğimiz için, AspNet Compatibility modun açık olması önemlidir.-->
    <serviceHostingEnvironment aspNetCompatibilityEnabled="true"/>
    <standardEndpoints>
      <webHttpEndpoint>
        <!--
            Configure the WCF REST service base address via the global.asax.cs file and the default endpoint
            via the attributes on the <standardEndpoint> element below
        -->
        <standardEndpoint name="" helpEnabled="true" automaticFormatSelectionEnabled="true"/>
      </webHttpEndpoint>
    </standardEndpoints>
  </system.serviceModel>

</configuration>

Buradaki ayarlamalara göre CategoriesCache adı ile belirlenen operasyonlar 120 saniye süreyle ön bellekten getirilecektir. Ön bellekleme ortamının sunucu taraflı olduğu location niteliği ile belirtilmektedir. Bu ön bellek ayarında parametre kullanılmadığı(varyByParam="none") ve talebin(request) Header kısmına göre Accept tipinde olanların göz önüne alındığı ifade edilmektedir. Diğer yandan CategoriesByFirstLetterCache bildirimine bakıldığında sürenin 300 saniye olduğu ama bir öncekinden farklı olarak firstLetter parametresine göre ön bellekte ayrı ayrı görüntüler tutulacağı belirtilmektedir. Buna göre Categories/A gibi bir talep ile Categories/s gibi bir taleb için üretilen HTML çıktıları ön bellekte ayrı ayrı tutulacaktır. Tabi birden fazla parametrenin değerlendirilmesi gerektiği durumlarda ; işareti kullanılarak bildirilmeleri gerekmektedir. Örneğin varyByParam="firstLetter;productCount" vb.

Tabi yapımış olduğumuz bu anlatımın sonuçlarını test ederek görmemiz gerekiyor. Burada debug modu kullanarak talepler sırasında operasyon metodlarının içerisine ne zaman girilip ne zaman girilmediğini tespit ederek gerekli kontrolleri yapabiliriz. İşte size örnek test senaryoları;

1 - Önce URL üzerinden Categories talebini gönderin. Bu durumda tüm kategorilerin aşağıdaki şekilde olduğu gibi geldiğini göreceksiniz.

Şimdi 120 saniyelik süre dolmadan Kategoriler.txt üzerinde bir değişiklik yapın. Örneğin Mücehver isimli yeni bir kategori ekleyin veya bir kaç kategorinin ismini değiştirin yada silin. 120 saniyelik zaman dilimi içerisinde yeniden Categories talebinden bulunursanız yapmış olduğunuz değişiklilerin tarayıcıya getirilmediğini görebilirsiniz. Ancak 120 saniyelik ön bellek süresi dolduktan sonra değişiklikleri görebileceksiniz ve hatta Debug moddaysanız ilgili operasyon kodu içerisine tekrardan girildiğini fark edeceksiniz. Bu zaten ön belleklemenin çalıştığının ispatıdır.

2 - İkinci olarak parametre bazlı ön bellekleme yapıldığını test edebilirsiniz. Bu amaçla tarayıcı üzerinden örneğin Categories/A ve Categories/M taleplerinde bulunun. Debug modda ilerlerseniz testinizi daha başarılı bir şekilde yapabilirsiniz. Özellikle 300 saniyelik ön bellekleme süreleri dolmadan farklı harfler ile denemeler yaptığınızda, ön belleklenenler için operasyon koduna girilmediğini ama daha önceden talep edilmeyenler veya 300 saniyelik ön bellekte kalma süresini dolduranlar için kodun tekrar çalıştırıldığını gözlemleyebilirsiniz.

Bu sayede geliştireceğimiz WebHttp Service' lerin operasyonlarının hızlı sonuçlar üreterek daha performanslı ve verimli olmasını sağlayabiliriz. Bu yazımızdaki örneğimizde sunucu tarafında ön bellekleme işlemlerini gerçekleştirdik. Ancak birde istemci taraflı ön bellekleme(Client Based Caching) işlemlerinin söz konusu olduğunu belirtelim. Bunu serinin sonraki yazısında incelemeye çalışacağız. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Lesson6_RC.rar (20,41 kb) [Örnek Visual Studio 2010 Ultimate Beta 2 Sürümünde geliştirilmiş ancak RC sürümü üzerinde de test edilmiştir]

WCF WebHttp Services - Routing

Pazartesi, 8 Mart 2010 08:45 by bsenyurt

Merhaba Arkadaşlar,

Geçtiğimiz aylarda yurdumuzun en güzel şehirlerinden birisi olan İstanbul' da, oldukça soğuk ve kar yağışlı günler geçirdik. Yandaki resimde görülen manzara çalıştığım ARI 1 binasının yürüyüş yolu üzerinde çok da yeni olmayan, dokunmatik ekran, Windows Mobile gibi ileri özellikleri bulunmayan ancak Carl Zeiss mercekli Sony Ericsson K810i marka cep telefonum tarafından çekilmiştir. 3.1 Megapiksel ölçekli çekilen fotoğrafı kaliteli yapan ise elbetteki Carl Zeiss mercek. İnsan böyle zamanlarda eğer şartlar yerindeyse(çay, kahve, sıcak ve sessiz bir ortam, hızlı bir internet bağlantısı) işiyle ilgili pek çok konuda araştırma fırsatı bulabiliyor. Malum böyle havalarda sizde benim gibi evden çıkmamayı veya mesai sonrası şirkette bir kaç saat durmayı tercih edinenlerdenseniz, yapılacaklar listesinin belkide en başında yenilikleri araştırmak vardır diye düşünüyorum. Malum bir süredir de WCF Eco System' in önemli parçalarından birisi olan WebHttp Service' lerini incelemekte olduğumuza göre yine bu alanda ilerleyerek devam edebiliriz. Bu günkü konumuz ise WCF WebHttp servislerinde yönlendirme(Routing) işlemleri olacak.

Pek tabi, bir WCF REST Service Application içerisinde birden fazla WebHttp Service sınıfı konuşlandırılabilir. Bu çoğunlukla tek bir sınıfın var olduğu hallerde zaman içerisinde artan fonksiyon sayısı nedeniyle kavramsal bütünün bozulmasını engellemek amacıyla alınan bir tedbir olarak düşünülebilir. Aslında gerçek hayatta söz konusu kavramsal bütünlüğün ayrıştırılması ile ilişkili pek çok başarılı örnek yer almaktadır. Örneğin Sql Server 2005 ile gelen meşhur Adventure Works veritabanını göz önüne alalım. Burada bildiğiniz üzere çok sayıda tablo çeşitli şemalara(Schema) ayrılmıştır. Production, HumanResource, Sales vb...Böylece veritabanı alan modelinin(Domain Model) kavramsal olarak kolay bir şekilde ayrıştırılması mümkün olmaktadır. İşte benzer durum servis sınıflarının içeriğini oluşturan fonksiyonlar için de geçerli olabilir. Buna göre operasyonların ayrı servis sınıfları altında toplanıyor olması Adventure Works örneğindekine benzer kavramsal bir ayrımın yapılabilmesi manasına gelmektedir. Peki bir servisin yürüttüğü operasyonları sınıf bazında ayrıştırmanın uygulanışında nelere dikkat edilmesi gerekmektedir?

Herşeyden önce WebHttp Service' leri URI üzerinden gelen HTTP taleplerini değerlendirmektedir. Yani servise gelen bir talep, servis sınıfı içerisindeki bir operasyon ile ilişkilendirilmektedir. Bu noktada WCF WebHttp Service' lerinin konuşlandırıldığı Web uygulamasının global.asax içeriğinin büyük önemi vardır. Nitekim bu dosya içerisinde gelen talep için bir yönlendirme yapılması mümkündür. Bu amaçla RegisterRoutes isimli metoddan yaralanılır. Bir başka deyişle servisleri barındıran Web uygulaması ayağa kaldırıldığında, hangi URI taleplerinin hangi servislere yönlendirileceği bellidir. URI içerisinde yer alan operasyonel yönlendirmeler ise ilgili servis sınıfının metodlarına ait WebGet veya WebInvoke nitelikleri yardımıyla belirlenmektedir. Dolayısıyla istemcilerden gelecek olan taleplerin hangi servise yönlendirileceği sorusunun cevabı global.asax dosyası içerisinde verilmektedir. Hemen küçük bir not düşelim; bildiğiniz üzere Web taleplerinin daha etkin bir şekilde yönlendirilebilmesi yeteneği Asp.Net 4.0 ile birlikte gelmektedir. Bu sebepten geliştirilen WCF WebHttp servisinin Asp.Net uyumluluğu modunda(Asp.Net Compatibility Mode) çalıştırılıyor olması önemlidir.

Dilerseniz konuyu daha net kavrayabilmek adına basit bir örnek ile devam edelim. Bu amaçla Visual Studio 2010 Ultimate RC sürümü üzerinden geliştireceğimiz WCF REST Service Application'a ait Solution içeriğini aşağıdaki gibi tasarladığımızı düşünelim.

Görüldüğü üzere CalculationService ve ProductionService isimli iki sınıfımız bulunmaktadır. Bu servis sınıfları içerisinde çok basit iki operasyon yer almakta ve her ikiside HTTP Get metodlarına göre cevap vermektedir. ProductionService sınıfı içerisinde Product tipinden değer döndüren bir arama operasyonu yer almaktadır. Diğer yandan CalculationService sınıfı içerisinde bir ürün fiyatına hangi günde bulunulduğuna göre indirim yapan ve sonuç değerini gösteren bir operasyon yer almaktadır. Yönlendirme teorimize göre servis uygulamasına gelen talepleri CalculationService ve ProductionService tipleri üzerine dağıtmak istiyoruz. Bunun için global.asax dosyasında gerekli kodlamaları yapmamız gerekecektir. Ama öncesinde CalculationService, ProductionService ve Product tipi içeriklerimize bir bakalım.

Sınıf diagramımız;

Product sınıfımız;

namespace Lesson5
{
    public class Product
    {
        public int ProductId { get; set; }
        public string Name { get; set; }
        public decimal ListPrice { get; set; }
    }
}

ProductionService sınıfımız;

using System.Collections.Generic;
using System.Linq;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;

namespace Lesson5
{
    [ServiceContract]
    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
    public class ProductionService
    {
        private static List<Product> Products = new List<Product>
        {
            new Product{ ProductId=1023, Name="Microsoft Optical Mouse", ListPrice=34.95M},
            new Product{ ProductId=1045, Name="Logitech Optical Mouse", ListPrice=35.45M},
            new Product{ ProductId=1029, Name="KeyMaster Wireless Keyboard", ListPrice=55.99M},
            new Product{ ProductId=9802, Name="Obi Wan Kneobi Jedi Light Saber", ListPrice=334.45M},
        };

        [WebGet(UriTemplate = "Products/{ProductId}")]
        public Product GetProduct(string ProductId)
        {
            Product prd=null;

            try
            {
                prd = (from p in Products
                       where p.ProductId.ToString() == ProductId
                       select p).First();
            }
            catch
            {          
                // Bir Product bulunamama olasılığına karşın HTTP statü kodu 404 döndürülür
                throw new WebFaultException(System.Net.HttpStatusCode.NotFound);
            }

            return prd;
        }
    }
}

CalculationService sınıfımız;

using System;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;

namespace Lesson5
{
    [ServiceContract]
    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
    public class CalculationService
    {
        [WebGet(UriTemplate = "Discount/{ListPrice}")]
        public decimal CalculateDiscountListPrice(string ListPrice)
        {
            decimal decreasedListPrice = 0;
            decimal currentListPrice = 0;

            // Eğer string olarak gönderilen ListPrice değeri decimal tipe dönüşütürülemesse BadRequest tipinden bir statü kodu döndürülmesi sağlanır.
            if (!decimal.TryParse(ListPrice, out currentListPrice))
                throw new WebFaultException(System.Net.HttpStatusCode.BadRequest);

            switch (DateTime.Now.DayOfWeek)
            {
                case DayOfWeek.Friday:
                    decreasedListPrice = currentListPrice-(currentListPrice * 0.1M);
                    break;
                case DayOfWeek.Monday:
                    decreasedListPrice = currentListPrice-(currentListPrice * 0.2M);
                    break;
                case DayOfWeek.Saturday:
                    decreasedListPrice = currentListPrice;
                    break;
                case DayOfWeek.Sunday:
                    decreasedListPrice = currentListPrice;
                    break;
                case DayOfWeek.Thursday:
                    decreasedListPrice = currentListPrice-(currentListPrice * 0.3M);
                    break;
                case DayOfWeek.Tuesday:
                    decreasedListPrice = currentListPrice-(currentListPrice * 0.5M);
                    break;
                case DayOfWeek.Wednesday:
                    decreasedListPrice = currentListPrice-(currentListPrice * 0.2M);
                    break;
            }

            return decreasedListPrice;
        }
    }
}

Ve gelelim projemizin en önemli kısmına. Yönlendirme ile ilişkili olarak global.asax dosyasında yer alan RegisterRoutes metodunun içeriğini aşağıdaki gibi değiştirmemiz yeterli olacaktır.

using System;
using System.ServiceModel.Activation;
using System.Web;
using System.Web.Routing;

namespace Lesson5
{
    public class Global
        : HttpApplication
    {
        void Application_Start(object sender, EventArgs e)
        {
            RegisterRoutes();
        }

        private void RegisterRoutes()
        {
            // WebServiceHostFactory nesnesi örneklenir.
            WebServiceHostFactory hostFactory = new WebServiceHostFactory();

            // Route tablosuna gerekli eşleştirme bilgileri eklenir.
            // Gelen talep Productions içinse ProductionService servis tipi ile ilişkilendirilir.
            // Gelen talep Calculations içinse CalculationService servis tipi ile ilişkilendirilir.
            RouteTable.Routes.Add(new ServiceRoute("Productions", hostFactory, typeof(ProductionService)));
            RouteTable.Routes.Add(new ServiceRoute("Calculations", hostFactory, typeof(CalculationService)));
        }
    }
}

Uygulama başlatıldığında Application_Start metoduna girilecektir. Bu metod içerisinde ise RegisterRoutes fonksiyonu çağırılmaktadır. RegisterRoutes metodu içerisinde dikkat edileceği üzere iki ServiceRoute tipinin örneklenmesi ve bunların RouteTable üzerindeki Tables koleksiyonuna eklenmesi sağlanmaktadır. Buna göre servise gelen talepler aşağıdaki şekilde görüldüğü üzere eşleşen tiplere yönlendirilecektir.

Hemen çalışma zamanı sonuçlarına bir bakalım. Örneğin http://localhost:10860/Productions/Products/1029 talebinde bulunduğumuzda aşağıdaki sonuçlar ile karşılaşırız. Görüldüğü üzere talep ProductionService' e yönlendirilmiştir. (Tabi var olmayan bir ProductID talebi girildiğinde HTTP Status Code 404 NotFound ile karşılaşırız. WebFaultException tipinin kullanımının anlatıldığı yazımızı hatırlayalım lütfen Wink )

Diğer yandan http://localhost:10860/Calculations/Discount/120,45 şeklinde bir talepte bulunduğumuzda ise CalculationService' e yönlendirildiğimizi görebiliriz. (Yine decimal tipe dönüştürülemeyen bir talep gönderildiğinde tahmin edileceği üzere HTTP Status Code 400 Bad Reuqest ile karşılaşırız.)

Sonuç olarak UriTemplate' lerin ilgili servis operasyonları ile eşleştirilmesi çok daha kolaylaştırılmıştır. Öncelikle olarak gelen talebin hangi servis ile ilişkili olduğu noktasında Route Table devreye girmektedir. Ardından talep servise gelir. Servis ise URI içeriğine bakara uygun operasyonun çağırılması ile ilgilenir. Böylece geldik bir konumuzun daha sonuna. Bir sonraki yazımızda görüşünceye dek hepinize mutlu günler dilerim.

Lesson5_RC.rar (24,11 kb) [Örnek Visual Studio 2010 Ultimate Beta 2 Sürümünde geliştirilmiş ancak RC sürümü üzerinde de test edilmiştir]

WCF WebHttp Services - Error Handling

Cuma, 5 Mart 2010 08:10 by bsenyurt

Merhaba Arkadaşlar,

Bu yazımızda WCF Eco System' in bir parçası olan WebHttp Service' lerinde hata yönetimini(Error Management) etkili bir şekilde nasıl ele alabileceğimizi incelemeye çalışıyor olacağız. WCF WebHttp Service' leri üzerinden çağırılan bir servis operasyonundan, istemci tarafına kendi insiyatifimizde hata mesajları gönderilmesini sağlayabiliriz. Üstelik bu mesajları bilinen HTTP durum kodları(HTTP Status Code) çerçevesinde yayınlayabiliriz. Bu tip bir isteğin çeşitli sebepleri olabilir. Hemen bir gerçek hayat senaryosu üzerinden ilerleyerek bu basit konuyu pekiştirmeye çalışalım. Bu amaçla Visual Studio 2010 Ultimate RC sürümü üzerinde geliştirdiğimiz ve  aşağıdaki kod içeriğine sahip bir WCF REST Service Application projemiz olduğunu düşünelim.

using System.Collections.Generic;
using System.Linq;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;

namespace ServerApp
{
    [ServiceContract]
    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
    public class AzonBookService
    {
        static List<Book> books = new List<Book>
            {
                new Book{ Id=1002, Name="Programming WCF 4.0",ListPrice=12.00M},
                new Book{Id=9034,Name="Pro Asp.Net 4.0",ListPrice=34.90M},
                new Book{Id=4560,Name="Algebra",ListPrice=122.39M},
                new Book{Id=1200,Name="C# 3.5 Cookbook",ListPrice=14.45M},
                new Book{Id=1201,Name="Pro C# 4.0 and .Net Framework 4.0",ListPrice=34.05M},
                new Book{Id=1201,Name="Beginning Ado.Net Entity Framework",ListPrice=14.55M}
            };

        [WebGet(UriTemplate = "/{firstLetter}")]
        public List<Book> GetBooks(string firstLetter)
        {
            return (from book in books
                   where book.Name.StartsWith(firstLetter)
                   select book).ToList();
        }
    }

    public class Book
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public decimal ListPrice { get; set; }
    }
}

AzonBookService çok basit ve tek bir operasyona sahiptir. GetBooks isimli operasyon HTTP Get metoduna göre hizmet vermekte olup baş harfleri firstLetter parametresin ile gelen değer ile başlayan bir List<Book> koleksiyonunu geriye döndürmektedir. Bu servisi bir tarayıcı uygulama üzerinden test ettiğimizde ve örneğin Pro kelimesi ile başlayan kitapları elde etmek istediğimizde aşağıdaki ekran görütüsündekine benzer sonuçlar ile karşılaşırız.

Bu noktaya kadar zaten herhangibir sorun bulunmamaktadır. Ancak örneğin Ç harfi ile başlayan kitapların listesini elde etmek istediğimizde, aşağıdaki ekran görüntüsü ile karşılaşırız.

Şimdi bu noktada geliştirici olarak bir karar verebiliriz. İstemci tarafına bu şekilde boş bir XML içeriği döndürmemiz halinde, istemci tarafının Ç harfi ile başlayan kitapların olmaması durumunu ele alması gerekmektedir. Yani istemci tarafına ek bir iş yükü getirmiş olabiliriz. Nitekim istemcinin dönen listenin eleman sayısına bakara bir karar vermesi gerekmektedir. Diğer yandan istemciyi bilinçli bir şekilde uyarabiliriz de. Nasıl mı? Servis tarafındaki çalışma zamanında üreteceğimiz ve o anki vakaya uygun bir istisna(Exception) ile. Tabiki bu istisna mesajı servis bazlı bir yapı üzerinden ele alınacağı için istemci tarafına gönderilecek paketin içerisine gömülecektir. WCF WebHttp Service' lerinde bu tip istisnaları ele almak için WCF tarafından bildiğimiz FaultException tipinden türeyen WebFaultException sınıfından yararlanılır.

System.ServiceModel.Web isim alanı altında yer alan WebFaultException tipinin normal ve generic olan versiyonları bulunmaktadır. İlgili tipleri kullanımları son derece basittir. Yukarıdaki senaryomuza göre istemciye aradığı kriterlere uygun bir içerik bulunamadığını bildirmek amacıyla, GetBooks isimli servis operasyonunu aşağıdaki gibi değiştirebiliriz.

[WebGet(UriTemplate = "/{firstLetter}")]
        public List<Book> GetBooks(string firstLetter)
        {
            var result=(from book in books
                   where book.Name.StartsWith(firstLetter)
                   select book).ToList();

            if (result.Count == 0)
                throw new WebFaultException<string>("Talep edilen kelime ile başlayan kitaplar sistemde mevcut değiller.", System.Net.HttpStatusCode.NotFound);

            return result;
        }

Burada dikkat edileceği üzere sonuç listesinin eleman sayısı kontrol edilmiş ve eğer 0 ise WebFaultException tipinden bir istisna mesajı fırlatılması sağlanmıştır. WebFaultException tipinin örneklenmesi sırasında dikkat edilmesi gereken hususlardan biriside HttpStatusCode.NotFound Enum sabiti değerinin verilmesidir. Bu şekilde istemci tarafına hangi HTTP durum kodunun(Status Code) gönderileceği belirlenmektedir. Tahmin edeceğiniz üzere pek çok HTTP Status Code değeri bulunmaktadır.

Aslında tam liste içeriği şudur Wink Accepted, Ambiguous, BadGateway, BadRequest, Conflict, Continue, Created, Expectation Failed, Forbidden, Found, GatewayTimeout, Gone, HttpVersionNotSupported, InternalServerError, LengthRequired, MethodNotAllowed, Moved, MovedPermanently, MultipleChoices, NoContent, NonAuthoritativeInformation, NotAcceptable, NotFound, NotImplemented, NotModified, OK, PartialContent, PaymentRequired, PreconditionFailed, ProxyAuthenticationRequired, Redirect, RedirectKeepVerb, RedirectMethod, RequestedRangeNotSatisfiable, RequestEntityTooLarge, RequestTimeout, RequestUriTooLong, ResetContent, SeeOther, ServiceUnavailable, SwitchingProtocols, TemporaryRedirect, Unauthorized, UnsupportedMediaType, Unused, UseProxy

Operasyonumuzu bu yeni haliyle denediğimizde ise tarayıcı uygulama üzerinde aşağıdaki şekilde görülen çıktı ile karşılaşırız.

ki bu son derece doğaldır.

Nitekim zaten HTTP 404 mesajının istemci tarafına gönderilmesi sağlanmaktadır. WebFaultException<T> sınıfının örneklenmesi sırasında T parametresi de önemlidir. Örneğimizde basit bir string tipi kullanılmıştır. Peki ya kullanıcı tanımlı bir tip buraya dahil edilebilir mi? Gelin hem bu durumu hemde istemci uygulamanın geliştiriciler tarafından yazılması halinde söz konusu hata mesajlarının nasıl ele alındığı örneğimizi güncelleyerek irdelemeye çalışalım. Bu amaçla servisimizi aşağıdaki hale getirelim.

...Kodun diğer kısmı
       
        [WebGet(UriTemplate = "/{firstLetter}")]
        public List<Book> GetBooks(string firstLetter)
        {
            var result=(from book in books
                   where book.Name.StartsWith(firstLetter)
                   select book).ToList();

            if (result.Count == 0)
                throw new WebFaultException<ErrorInformation>(
                    new ErrorInformation { SearchLetter = firstLetter, SearchTime = DateTime.Now, Summary = "Talep edilen kelime ile başlayan kitaplar sistemde mevcut değiller." }
                    , System.Net.HttpStatusCode.NotFound);

            return result;
        }
    }

    public class ErrorInformation
    {
        public string SearchLetter { get; set; }
        public DateTime SearchTime { get; set; }
        public string Summary { get; set; } 
    }

Bu kod parçasında WebHttpException<ErrorInformation> tipinden bir nesne örneklenmiştir. Buna göre istemci tarafına bu içeriğin XML veya JSON formatında gönderilmesi mümkündür. Gelelim istemci tarafının kodlarına. Nitekim bir şekilde servis tarafından gelen içeriği kontrol atlına almamız gerekmekte.

using System;
using Microsoft.Http;

namespace ClientApp
{
    class Program
    {
        static void Main(string[] args)
        {
            using (HttpClient client = new HttpClient("http://localhost:12654/Azon/"))
            {
                HttpResponseMessage response=client.Get("Ç");

                Console.WriteLine("Http Status Code : {0}\n",response.StatusCode.ToString());
                Console.WriteLine("Content : {0}\n",response.Content.ReadAsString());

            }
        }
    }
}

Ç harfi ile başlayan kitapların elde edilmesi için bir talep oluşturulmakta ve bu talep Get metodu yardımıyla servis tarafına gönderilmektedir. Get metodunun çıktısı HttpResponseMessage tipindendir. Elde edilen HttpResponseMessage nesne örneğinin StatusCode özelliği yardımıyla servis tarafında üretilen HttpStatusCode Enum sabitinin değeri elde edilir. Diğer yandan eğer istemci tarafından bir hata oluşuyorsa WebFaultException nesnesinin oluşturulması sırasında kullanılan ErrorInformation nesne örneğinin serileştirilmiş hali Content özelliği üzerinden elde edilebilmektedir. Buna göre çalışma zamanı çıktısı aşağıdaki gibi olacaktır.

Görüldüğü üzere servis operasyonu içerisinde üretilen ErrorInformation bilgisi istemci tarafına XML formatlı olacak şekilde aktarılmıştır. Sonuç olarak olası veya beklenen hatalar karşısında istemci tarafına uygun bir bildirim yapılması HTTP durum kodları bazında mümkündür. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Lesson4_RC.rar (174,57 kb) [Örnek Visual Studio 2010 Ultimate Beta 2 Sürümünde geliştirilmiş ancak RC sürümü üzerinde de test edilmiştir]

WCF WebHttp Services - JSON Formatlı Response Üretmek

Cuma, 19 Şubat 2010 07:50 by bsenyurt

Merhaba Arkadaşlar,

Yandaki Logo size neyi çağırıştırıyor? Aslında bakarsanız çok meşhur olan hafif siklette bir veri değiş tokuş formatının logosunu ifade etmekte. JSON(JavaScript Object Notation). Hatırlayacağınız üzere bir süredir WCF Eco System içerisinde yer alan WCF WebHttp Service alt yapısını incelemeye çalışıyoruz. WCF WebHttp Service' leri eğer istemci tarafından aksi belirtilmezse varsayılan olarak XML formatında çıktı üretmektedir. Ancak istenirse JSON(JavaScript Object Notation) formatında çıktı üretmeside sağlanabilir. Söz konusu çıktı üretim işlemi iki yolla gerçekleştirilebilir. Bilinçli olarak(Excplicitly) veya otomatik olarak. Bu yazımızda söz konusu yolları inceleyerek JSON formatında çıktıları nasıl verebileceğimizi basit bir örnek üzerinden görmeye çalışıyor olacağız. Örnek uygulamamızı bu kez Visual Studio 2010 Ultimate RC ortamı üzerinde geliştirdiğimizi belirtelim. Dolayısıyla ilerleyen sürümde bazı farklılıklar olabilir. Lesson3 isimli WCF REST Service Application uygulamamız içerisinde yer alan servis sınıfı içeriğimiz çok basit olarak aşağıdaki kod parçasından oluşmaktadır.

using System;
using System.Collections.Generic;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;

namespace Lesson3
{
    [ServiceContract]
    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
    public class PersonalityService
    {
        [WebGet(UriTemplate = "AllPersons")]
        public List<Person> GetAllPersons()
        {
            return new List<Person>()
            {
                new Person() { Id = 1, Name = "Burak Selim Şenyurt",Birth=new DateTime(1976,12,1) } ,
                new Person() { Id = 2, Name = "Bill Amca",Birth=new DateTime(1975,4,5) } ,
                new Person() { Id = 3, Name = "Luka Ton-i",Birth=new DateTime(1980,3,4) }
            };
        }
    }

    public class Person
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public DateTime Birth { get; set; }
    }
}

Servisimizde yer alan GetAllPersons isimli operasyon istemci tarafına Person tipinden bir liste içeriği döndürmektedir. Söz konusu operasyon HTTP protokolünün GET metoduna ait talepleri kabul etmektedir. Varsayılan olarak URL üzerinden yapacağımız AllPersons çağrısının sonucu aşağıdaki gibi olacaktır.

Ancak şimdiki hedefimiz bu XML çıktısı yerine JSON çıktısını vermektir. Bunu iki yol ile gerçekleştirebileceğimizden bahsetmiştik. Öncelikle otomatik JSON çıkıtısı üretiminin nasıl gerçekleştirilebileceğine bakalım. Bu amaçla sunucu tarafındaki web.config dosyası içerisinde yer alan webHttpEndpoint içerisindeki standardEndpoint elementinin automaticFormatSelectionEnabled niteliğinin true değere sahip olması yeterlidir. Aynen aşağıda görüldüğü gibi.

<system.serviceModel>
    <serviceHostingEnvironment aspNetCompatibilityEnabled="true"/>
    <standardEndpoints>
      <webHttpEndpoint>
        <standardEndpoint name="" helpEnabled="true" automaticFormatSelectionEnabled="true"/>
      </webHttpEndpoint>
    </standardEndpoints>
  </system.serviceModel>

Peki bu otomatikliğin anlamı nedir? Undecided Nitekim herhangibir yerde JSON çıktısı vereceğimizi belirtmedik. Dolayısıyla birisinin bunu talep ediyor olması gerekmekte. Tahmin edeceğiniz üzere burada sorumluluk istemci tarafına ait. Bir başka deyişle istemci uygulama talebini gönderirken JSON formatında bir içerik istediğini servis tarafına bildirmelidir. Dolayısıyla örneğimize aşağıdaki kodları içeren istemci uygulamayı yazarak devam etmeliyiz.

Not : İstemci uygulama açısından önem arz eden konulardan biriside, HttpClient tipinin kullanımı için gerekli olan WCF REST Starter Kit Preview 2 assmebly' ları ile ReadAsJsonDataContract genişletme metodunun(Extension Methods) kullanımı için gerekli olan System.ServiceModel.Web ve System.Runtime.Serialization assembly' larını referans etmesidir.

Buna göre istemci tarafının kodlarını aşağıdaki şekilde geliştirebiliriz.

using System;
using Microsoft.Http;

namespace ClientApp
{
    class Program
    {
        static void Main(string[] args)
        {
            using (HttpClient client = new HttpClient("http://localhost:2360/"))
            {
                HttpRequestMessage request = new HttpRequestMessage("GET", "AllPersons");               
                request.Headers.Accept.AddString("application/json");
                HttpResponseMessage response = client.Send(request);
                response.EnsureStatusIsSuccessful();
                HttpContent content=response.Content;
                Console.WriteLine(content.ReadAsString());
            }
        }
    }
}

Bu kod parçasında en çok dikkat edilmesi gereken nokta talep ile ilişkili Header kısmına eklenen application/json bilgisidir. Bu durumda HTTP Get metoduna göre yapılan servis çağrısı çıktısının JSON formatında olması istenmektedir. Servis tarafında da gelen isteğe göre bir çıktı üretildiğinden, WCF çalışma zamanı operasyon çıktısını JSON formatına dönüştürecektir. Kodun çalışması sonrasında aşağıdaki ekran çıktısı ile karşılaştığımızı görürüz.

Dikkat edileceği üzere JSON formatında bir çıktı elde edilmiştir.

İstemci tarafına gelen bu çıktının Person tipini içeren bir koleksiyon şeklinde ele alınması istediğimizdeyse HttpContent tipi üzerinden System.Runtime.Serialization.Json isim alanında yer alan ReadAsJsonDataContract genişletme metodunu çağırabiliriz. Tabi burada istemci tarafında Person tipininde bir örneğinin yer aldığını varsayıyoruz ki bunu bildiğiniz üzere WCF REST Starter Kit Preview 2 ile gelen Paste XML As Types seçeneği ile oluşturabiliriz. Eğer hatırlamıyorsanız biraz araştırmaya ne dersiniz? Wink İşte istemci tarafındaki yeni kod içeriğimiz.

using System;
using System.Collections.Generic;
using System.Runtime.Serialization.Json;
using Microsoft.Http;

namespace ClientApp
{
    class Program
    {
        static void Main(string[] args)
        {
            using (HttpClient client = new HttpClient("http://localhost:2360/"))
            {
                HttpRequestMessage request = new HttpRequestMessage("GET", "AllPersons");               
                request.Headers.Accept.AddString("application/json");
                HttpResponseMessage response = client.Send(request);
                response.EnsureStatusIsSuccessful();
                HttpContent content=response.Content;
                //Console.WriteLine(content.ReadAsString());
            
                List<Person> personList=response.Content.ReadAsJsonDataContract<List<Person>>();

                foreach (Person person in personList)
                {
                    Console.WriteLine("{0} {1} {2}",person.Id,person.Name,person.Birth.ToString());
                }
            }
        }
    }
}

Bu durumda çalışma zamanında aşağıdaki sonucu elde ederiz.

Gelelim bilinçli olarak çıktı formatının nasıl belirleneceğine. Öncelikli olarak neden bilinçli bir şekilde format çıktısını söylememiz gerektiğini kavramamızda yarar olduğu kanısındayım. İstemci tarafının her zaman HTTP talebinin Header kısmına müdahale etmesi söz konusu olamayabilir. Böyle bir durumda istemcinin JSON formatında talepte bulunabilmesi de mümkün değildir. Dolayısıyla bu tip bir vakada JSON formatında çıktı verileceğinin bilinçli olarak bildirilmesi gerekmektedir. Peki ya nerede ve nasıl? Cevap: Servis tarafındaki ilgili operasyon içerisinde ve bir parça kod yardımıyla Wink İşte GetAllPersons isimli servis operasyonumuzun bilinçli olarak JSON formatında çıktı veren yeni versiyonu.

using System;
using System.Collections.Generic;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;

namespace Lesson3
{
    [ServiceContract]
    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
    public class PersonalityService
    {
        [WebGet(UriTemplate = "AllPersons?whichFormat={format}")]
        public List<Person> GetAllPersons(string format)
        {
            if (format.ToLower().Equals("json"))
            {
                WebOperationContext.Current.OutgoingResponse.Format = WebMessageFormat.Json;
            }
            return new List<Person>()
            {
                new Person() { Id = 1, Name = "Burak Selim Şenyurt",Birth=new DateTime(1976,12,1) } ,
                new Person() { Id = 2, Name = "Bill Amca",Birth=new DateTime(1975,4,5) } ,
                new Person() { Id = 3, Name = "Luka Ton-i",Birth=new DateTime(1980,3,4) }
            };
        }
    }

    public class Person
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public DateTime Birth { get; set; }
    }
}

İlk dikkat edilmesi gereken nokta, WebGet niteliğinde belirtilen format isimli parametre ile istemciden hangi formatta çıktı almak istendiğinin sorulmasıdır. Eğer json kelimesi yazılmışsa WebOperationContext üzerinden güncel çalışma zamanı içeriğine geçilerek cevap formatının JSON olacağı belirtilir ki buda dikkat edilmesi gereken ikinci noktadır. Tahmin edileceği üzere json dışında bir kelime girildiği takdirde varsayılan XML çıktısının üretilmesi söz konusu olacaktır. Servis operasyonumuzun bu son haline göre Internet Explorer üzerinden http://localhost:2360/AllPersons?whichFormat=json şeklinde bir talepte bulunursak, içeriği kaydetmemiz için bir iletişim penceresi ile karşılaşırız. İçeriği kaydettikten sonra Notepad programı ile açacak olursa aşağıdaki içeriğin üretildiğini görebiliriz.

ki buda tam anlamıyla JSON çıktısıdır. Smile Çıktının JSON veya XML harici formatlarda olması da söz konusudur aslında. Bu formatların nasıl ele alınacağını ise ilerleyen yazılarımızda değerlendirmeye çalışıyor olacağız. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Lesson3_RC.rar (173,29 kb) [Örnek Visual Studio 2010 Ultimate Beta 2 Sürümünde geliştirilmiş ancak RC sürümü üzerinde de test edilmiştir]

WCF WebHttp Services - Client Tarafını Geliştirmek

Cuma, 5 Şubat 2010 09:45 by bsenyurt

Merhaba Arkadaşlar,

Sanırım pek çoğumuz piyangodan veya diğer şans oyunlarından kendilerine tonlarca para çıksa ne yapacağını düşünmüş veya hayal etmiştir. Açıkası kendi adıma hayat etmediğimi dile getirsem yalan söylemiş olurum. Ancak ben pek çoğumuz gibi yan yana bir kaç Ferrari' yi dizmektense bir kaç yere yatırım yapmayı hayal etmişimdir hep. Örneğin dünyanın sayılı bir kaç futbol kulübünün(Barcelona, Manchester United vb...) ve yazılım şirketinin(Microsoft, IBM vb...) hisselerinden satın alır ve şöyle güzel bir fon sepeti oluştururum. Neyse...Sözü niye piyangodan açtığımıza gelince...

Hatırlayacağınız üzere bir önceki yazımızda WCF WebHttp Service' leri ile tanımaya çalışmış ve konuyu pekiştirmek amacıyla basit bir Merhaba Dünya uygulaması geliştirmiştik. Tabi bu örneğimizde HTTP protokolünün yanlızca Get metodunu kullanmıştık. Dolayısıyla operasyonlarımızda sadece WebGet niteliklerinin uygulandığına şahit olduk. Ancak HTTP protokolüne göre Get dışında Post, Put ve Delete metodlarını da kullanabileceğimizi biliyoruz. Dikkat çekici bir diğer noktada örneğimizde Get metoduna göre talepte bulunurken basit bir tarayıcı uygulamadan faydalanmış olmamızdı. Oysaki kendi istemci uygulamamızı yazmak isteyebiliriz. Bu durumda istemci tarafından HTTP protokolünün Get, Post, Put ve Delete metodlarına uygun talepleri nasıl gerçekleştirebiliriz? Aslında olay servis tarafının istediği mesaj paketlerini istemci tarafında oluşturup göndermekten başka bir şey değildir. Yani talebin(Request) içeriğini hazırlamak ve dönen cevabı(Response) değerlendirmek.

İşte bu yazımızda söz konusu durumları ele alaraktan hem Post, Put, Delete metodlarının kullanımına bir örnek verecek hemde istemci tarafını geliştirmeye çalışacağız. Tabi öncesinde servis tarafını hazırlamamız gerekiyor. Bu örneğimizde herhangibir işe yaramasada konuyu anlamamızı kolaylaştıracak bir senaryomuz da olacak. Senaryomuza göre bir Piyango servisi tasarlayacağız. Wink Bu servis, istemcilerin yeni bir piyango bileti üretebilmesine, var olan piyango biletlerini çekebilmelerine, isterlerse biletlerini silmelerine veya güncellemelerine izin veren operasyonlar içerecek. Tabiki bu operasyonlarda HTTP protokolünün Get, Post, Put ve Delete metodları göz önüne alınıyor olacak. Dilerseniz hiç vakit kaybetmeden WCF REST Service Application uygulamasını oluşturarak işe başlayalım. Uygulamamızda bilet bilgilerinin saklanması ve depolanması amacıyla basit text dosyasından yararlandığımızı belirtmek isterim. Diğer taraftan bilet bilgileri için Ticket isimli yardımcı bir sınıfımızda yer almaktadır.

using System;

namespace Lesson2
{
    public class Ticket
    {
        public string Number { get; set; }
        public string Owner { get; set; }
        public DateTime TicketDate { get; set; }
        public bool NewOrUpdated { get; set; }

        public override string ToString()
        {
            return String.Format("{0}|{1}|{2}|Is New? {3}", Number, Owner, TicketDate.ToString(),NewOrUpdated.ToString());
        }
    }
}

LotteryService isimli WCF WebHttp Service içeriği ise aşağıda görüldüğü gibidir.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;
using System.Web;

namespace Lesson2
{
    [ServiceContract]
    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
    public class LotteryService
    {
        string filePath = HttpContext.Current.Server.MapPath("~\\Tickets.txt");

        [WebGet(UriTemplate = "Lottery/{Name}/{LastName}")]
        public List<string> GetMyTickets(string Name,string LastName)
        {
            return (from line in File.ReadAllLines(filePath)
                    where line.Contains(Name + LastName)
                    select line).ToList();
        }

        [WebInvoke(UriTemplate = "Lottery/Create/{Name}/{Surname}", Method = "POST")]
        public Ticket CreateTicket(string Name,string Surname)
        {
            Ticket createdTicket=new Ticket
            {
                Number = Guid.NewGuid().ToString(),
                Owner = String.Format("{0}{1}",Name,Surname),
                TicketDate = DateTime.Now
            };

            File.AppendAllLines(filePath,new String[]{createdTicket.ToString()});
            return createdTicket;
        }

        [WebInvoke(UriTemplate = "Lottery/Update/{TicketNumber}", Method = "PUT")]
        public string UpdateMyTicketNumber(string TicketNumber)
        {
            string ticket = (from line in File.ReadAllLines(filePath)
                         where line.Contains(TicketNumber)
                         select line).First();
            string[] infos=ticket.Split('|');

            string updatedTicket=string.Join("|",Guid.NewGuid().ToString(),infos[1],infos[2],"Is New? ",true.ToString());

            File.AppendAllLines(filePath, new string[]{updatedTicket});

            return updatedTicket;
        }

        [WebInvoke(UriTemplate = "Lottery/Delete/{TicketNumber}", Method = "DELETE")]
        public void DeleteMyTicket(string TicketNumber)
        {
            string[] newLines = (from line in File.ReadAllLines(filePath)
                           where !line.Contains(TicketNumber)
                           select line).ToArray();

            File.WriteAllLines(filePath, newLines);
        }
    }
}

Kod parçamızda HTTP Get,Post,Put ve Delete metodlarının kullanımlarına örnek olması açısından çeşitli servis operasyonlarının yer aldığı görülmektedir. Kritik olan noktalar WebGet ve WebInvoke niteliklerinin nasıl kullanıldığıdır. Servisimizin yardım sayfasına bakıldığında, istemci tarafında oluşturulması gereken Request paketlerinin nasıl olacağıda kolaylıkla görülebilir. Tabi Post ve Put metodlarında bir Request Body kullanılmamıştır. Bir başka deyişle Put ve Post işlemleri için gerekli bilgiler servis tarafına URL satırından gönderilmektedir.

Gelelim istemci tarafına.

İstemciyi basit bir WinForms uygulaması olarak tasarlayacağız. Önemli olan nokta ise, az önce tasarlanan WCF WebHttp Service' ini nasıl kullanabileceğimiz. Sonuçta HTTP Get, Post, Put ve Delete metodlarının istemci tarafından hazırlanması ve gönderilmesi gerekmekte. Üstelik servise ait bir WSDL içeriği ve dolayısıyla Proxy üretimi de söz konusu değil. Bu noktada WebChannelFactory, HttpWebRequest ve WebClient tiplerinden yararlanabileceğimizi biliyoruz. Ne varki WCF Rest Starter Kit Preview 2 ile birlikte gelen HttpClient sınıfı tamda bu tip servislerin tüketilmesi için geliştirilmiş durumda. Elbette bu kit içeriğinin, .Net Framework 4.0' ın final sürümü ile birlikte içeriye doğrudan dahil edileceğini tahmin etmekteyiz. Şimdilik Starter Kit ile gelen tipi kullanacağız. Bu sebepten Windows uygulamamıza gerekli referansları aşağıdaki şekildende görüleceği üzere eklememiz gerekiyor.

Dikkat edilmesi gereken noktalardan biriside istemci uygulamanın hedeflediği Framework profilidir. Söz konusu Starter Kit referansları ile çalışabilmek için istemci tarafının hedef profilinin .Net Framework 4.0 Client Profile değil(ki varsayılanı budur) .Net Framework 4.0 olması gerekmektedir.

Artık istemci için gerekli tüm ön hazırlıklar yapılmıştır. Şimdi dilerseniz servis fonksiyonelliklerini icra edebilmek amacıyla Form içeriğini aşağıdaki gibi düzenleyelim.

Form üzerindeki kontrolleri kullanarak bilet üretebilecek, bir bileti silip güncelleyebilecek yada var olan biletlerimizi görebileceğiz. Tabiki tüm bu fonksiyonellikler LotteryService isimli WCF HttpWeb Service üzerinden gerçekleştiriliyor olacak. İlk olarak bir kişinin sahip olduğu tüm biletleri listlemeye çalışalım. Bu noktada servis tarafındaki GetMyTickets operasyonu için bir çağrı yapılması gerekiyor. Söz konusu çağrı örneğin http://localhost:16088/LotteryService/Lottery/Coni/Vayt şeklinde olabilir. Nitekim WebGet niteliğinde belirtilen URI bilgisi bu şekildedir. Bu durumda istemci tarafında aşağıdaki kodlamayı yapabiliriz.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using System.Xml.Linq;
using Microsoft.Http;

namespace ClientApp
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void btnGetMyTickets_Click(object sender, EventArgs e)
        {
            // REST Starter Kit Preview 2' den gelen HttpClient tipi oluşturulur.
            // Parametre olarak WCF WebHttp Service' in base address bilgisi verilir.
            using (HttpClient client = new HttpClient("http://localhost:16088/LotteryService/"))
            {
                // Get talebi için gerekli URI bilgisi oluşturulur
                string requestUri = String.Format("Lottery/{0}/{1}", txtName.Text, txtSurname.Text);
                // Get metodu ile ilgili talep oluşturulur.
                HttpResponseMessage responseMessage=client.Get(requestUri);
                // Operasyonun başarılı olmaması halinde bir exception fırlatılması isteniyorsa EnsureStatusIsSuccessful metodu kullanılmalıdır.
                responseMessage.EnsureStatusIsSuccessful();
                // İçerik XML tipinden geldiği için bunu kod tarafında kolayca ele almak adına ReadAsXElement genişletme metodu(Extension Method) kullanılabilir. Lakin bu metod için System.Xml.Linq isim alanının referans edilmesi gerekir.
                XElement response = responseMessage.Content.ReadAsXElement();
            }
        }
    }
}

Servisin çalıştığını varsaydığımızda yukarıdaki metodun icra edilmesi sonucu debug zamanında aşağıdaki sonuçlara ulaştığımızı görebiliriz. (Tickets.txt dosyasında bazı ticket bilgileri olduğunu  ve Name için Coni, Surname için Vayt bilgilerinin girildiğini varsayıyoruz)

Dikkat edileceği üzere servis tarafındaki operasyonda List<string> tipinden olan operasyon dönüş tipi istemci tarafına, ArrayOfstring ve string isimli alt elementlerden oluşan bir XML içeriği olarak aktarılmıştır. Elbette XElement içeriğinin bu şekilde elde edilebiliyor olması yeterli değildir. Bu içeriğin ListBox kontrolü içerisine serpiştirilmesini de bekliyoruz. Dolayısıyla aşağıdaki kod ilavesini de yapmamız gerekiyor.

XElement response = responseMessage.Content.ReadAsXElement();

lstMyTickets.Items.Clear();
var tickets = from node in response.Elements()
                  select node.Value;

foreach (var ticket in tickets)
{
    lstMyTickets.Items.Add(ticket);
}

Dikkat edileceği üzere basit bir XLINQ sorgusu ile tüm elementlerin Value değerleri çekilmiştir. Elbette bu XLINQ sorgu ifadesini belirleyen kriter, servis tarafından dönen XML içeriğinin şemasıdır. Çalışma zamanında örnek bir kullanıcı için Get sorgusunu gerçekleştirdiğimizde aşağıdaki ekran görüntüsündekine benzer sonuçları elde ettiğimizi görürüz.

Şimdi yeni bir biletin oluşturulması için gerekli kodları yazalım.

private void btnCreateTicket_Click(object sender, EventArgs e)
        {
            using (HttpClient client = new HttpClient("http://localhost:16088/LotteryService/"))
            {
                string requestUri = String.Format("Lottery/Create/{0}/{1}", txtName.Text, txtSurname.Text);

                // Yeni bir Ticket oluşturmak için gerekli istek HTTP Post metoduna göre yapılmaktadır. Bu sebepten HttpClient tipinin Post metodu kullanılmıştır.
                // Gönderilen talepte herhangibir Request Body içeriği olmadığından HttpContent tipinin CreateEmpty metodu kullanılmıştır.
                HttpResponseMessage responseMessage = client.Post(requestUri, HttpContent.CreateEmpty());
                responseMessage.EnsureStatusIsSuccessful();
                // Oluşturulan yeni Ticket bilgisi istemci tarafına yine bir XML içeriği olarak dönmektedir. Üretilen içerik bilgi amaçlı olarak kullanıcıya gösterilir.
                XElement createdTicket = responseMessage.Content.ReadAsXElement();
                MessageBox.Show(createdTicket.ToString());
            }
        }

Get kullanımına benzer olmakla birlikte bu kez HttpClient tipinin Post metodundan yararlanılmaktadır. Post ve Put gibi metodlarda Request Body' sinin olması gerekebilir. Ancak bizim servis operasyonlarımız Request Body kullanmamaktadır. Bu nedenle ilgili parametreler HttpContent.CreateEmpty() metodu ile geçilmektedir. Bu kod parçasına göre çalışma zamanında bir bilet üretmek istediğimizde geriye aşağıdakine benzer sonuçların aktarıldığını görebiliriz.

Peki ya silme ve güncelleme işlemlerinden ne haber? Bu operasyonlar için istemci tarafında aşağıdaki kodları yazmamız yeterli olacaktır.

private void btnDeleteTicket_Click(object sender, EventArgs e)
        {
            // Öncelikle ListBox' ta seçili bir öğe olup olmadığına bakılır
            if (lstMyTickets.SelectedItem != null)
            {
                // Biletin numarası yani GUID bilgisi alınır.
                string ticketNumber = lstMyTickets.SelectedItem.ToString().Substring(0, 36);
                using (HttpClient client = new HttpClient("http://localhost:16088/LotteryService/"))
                {
                    // HTTP Delete metoduna göre bir talepte bulunulur.
                    string requestUri = String.Format("Lottery/Delete/{0}", ticketNumber);
                    // Delete talebi için HttpClient tipinin Delete metodundan yararlanılır. Bu metodun kullanımına göre herhangibir HTTP Request Body içeriği bildirilmesi gerekli değildir.
                    HttpResponseMessage responseMessage = client.Delete(requestUri);
                    responseMessage.EnsureStatusIsSuccessful();
                   
                }
            }
        }

        private void btnUpdateTicket_Click(object sender, EventArgs e)
        {
            if (lstMyTickets.SelectedItem != null)
            {
                string ticketNumber = lstMyTickets.SelectedItem.ToString().Substring(0, 36);
                using (HttpClient client = new HttpClient("http://localhost:16088/LotteryService/"))
                {
                    // Update talebi hazırlanır
                    string requestUri = String.Format("Lottery/Update/{0}", ticketNumber);
                    // Güncelleme isteği aslında HTTP Put metoduna karşılık gelmektedir. Bunun için HttpClient tipinin Put metodundan yararlanılır.
                    HttpResponseMessage responseMessage = client.Put(requestUri, HttpContent.CreateEmpty());
                    responseMessage.EnsureStatusIsSuccessful();
                    // Put metodunun çalıştırılması sonucu üretilen çıktı bu kez string bazlı olacak şekilde bir MessageBox aracılığıyla gösterilir.
                    MessageBox.Show(responseMessage.Content.ReadAsString());
                }
            }
        }

Örneğin var olan bir biletimizi güncellemek istediğimizi düşünelim. Örnek çalışma zamanı görüntüsü aşağıdakine benzer olacaktır.

Tabi uygulamamızın pek çok yerinde bug ve iş mantığı hatası vardır. Üstelik tam anlamıyla bir istisna yönetimide(Exception Handling) yapılmamaktadır. Ancak odaklanmamız gereken yada dikkat etmemiz gereken noktalar WCF WebHttp Service üzerinden Post, Put, Delete operasyonlarının nasıl sunulduğu ve istemci tarafında bunların nasıl ele alındığıdır. Özellikle istemci tarafında kullandığımız tekniklere baktığımızda şu sonuçlara varabiliriz;

  • Herhangibir proxy tipi kullanılmamıştır. Bilindiği üzere servisleri tüketmenin yollarından birisi istemci tarafında gerekli proxy tipinin üretilmesidir. HTTP Get,Post,Put ve Delete metodlarının kullanıldığı teknikte ise sadece paketlerin oluşturulup gönderilmesi ve cevapların değerlendirilmesi gerekmektedir.
  • Sonuçların ilgili formata göre istemci tarafına gönderilmesi söz konusudur. Varsayılan olarak XML tipinden içerik döndürülmektedir. Ancak JSON formatında da dönüşler olabilir.
  • Get metodundan elde edilen XML formatlı içerikler istemci tarafında XElement tipi ile ele alınabilir ve XLINQ kullanılarak ayrıştırılabilir.
  • İstemci tarafından yapılan güncelleştirme çağrıları için Put, ekleme işlemlerine ait talepler için Post, silme işlemlerine ait talepler için Delete metodları kullanılır.
  • Post ve Put metodları parametre olarak eğer gerekliyse bir Body içeriği sunmak zorunda olabilirler. Eğer sunmuyorlarsa HttpContent.CreateEmpty() metodu ile boş içerik gönderileceğinin belirtilmesi gerekir.

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

Lesson2.rar (196,61 kb) [Örnek Visual Studio 2010 Ultimate Beta 2 Sürümünde geliştirilmiş ancak RC sürümü üzerinde de test edilmiştir]

WCF WebHttp Services - Tanışma

Pazartesi, 1 Şubat 2010 14:50 by bsenyurt

Merhaba Arkadaşlar,

Nihayet taşlar yerli yerine oturmaya başladı. 2008 yılında düzenlenen Microsoft PDC' de  tanıtılan sürüm ile başlayan macerada Beta 1, Beta 2 versiyonları derken yavaş yavaş RC, RTM sürümlerinin çıkacağı günlere gelmekteyiz. Elbette hepizimin beklentisi bir an önce stabil bir sürüme kavuşabilmek. Bu günlerde çok doğal olarak .Net Framework 4.0 ve Visual Studio 2010 ürünlerinin sınırlarının daha da netleştiğini görmeye başladık. Her ne kadar henüz yayınlanmış yeni bir sürüm olmasa da, pek çok güncel ve geçerli kaynaktan okuduğumuz kadarı ile bu böyle. Taşların yerli yerine oturmaya başladığı ve herşeyin biraz daha belirginleştiği alanlardan biriside Windows Communication Foundation 4.0.

Hatırlayacağınız üzere WCF Eco System' i anlattığımız yazımızda, WCF alt yapısı üzerine geliştirilen ve amaca yönelik olarak farklılaştırılan servis geliştirme modellerine değinmiştik. Bunlardan biriside WebHttp Services idi. Bu yazımız ile birlikte WebHttp Service' lerini tanımaya çalışacağız. Aslında WCF 3.5 sürümüne kazandırılan Web programlama teknikleri sayesinde zaten uzun bir süredir farkında olduğumuz non-SOAP bazlı bir modelden bahsediyoruz. Bildiğiniz üzere WebGet ve WebInvoke isimli nitelikler(attribute) yardımıyla servis operasyonlarının HTTP Get,Post,Put ve Delete metodlarına cevap verebilecek şekilde tasarlanması mümkün. Ancak zaman ilerledikçe REST(REpresentational State Transfer) modeline göre WCF servislerinin daha kolay geliştirilmesini sağlayan ve WCF 4.0 içerisinde gömülecek yeni özelliklerin bir ön görünümünü bizlere sunan WCF REST Starter Kit ile karşılaştık.

WCF Eco System' in bir parçası olan WebHttp Service' ler, .Net Framework 3.5 ile gelen Web Programlama modeli, REST Starter Kit ile tanıtılan kabiliyetler ve bunlara ek yeni özelliklerin .Net Framework 4.0 içerisinde ele alınmasını sağlayan bir geliştirme alt yapısı olarakta düşünülebilir. Şimdi dilerseniz WebHttp Service' lere bir merhaba demeye çalışalım. Örneğimizi Visual Studio 2010 Ultimate Beta 2 sürümü üzerinden geliştirmeye çalışıyor olacağız. Ancak yazıyı hazırladığım tarihte araştırdığım MSDN, The .NET Endpint vb blog sitelerinde yer alan bilgilere göre Visual Studio 2010' un o anki sürümü üzerinde WebHttp Service' leri için bir proje şablon(project template) bulunmamaktaydı. Bu nedenle öncelikli olarak online template' lerden WebHttp Service için olanları indirmemiz gerekiyor. Bu amaçla Visual Studio 2010 ortamında Tools->Extensions Manager->Online Gallery kısmına geçiş yapıp WCF ile ilişkili olan şablonlardan WCF Rest Service Template' i indirmemiz gerekiyor. Ben 4.0 versiyonunun C# programlama dili destekli olanını indirdim. Son sürümde büyük ihtimalle şu anda online olarak indirdiğimiz bu şablonun ve başka diğer şablonların Visual Studio 2010 içerisine gömülü olarak geleceğini ümit etmekteyim.

Download ve Install işlemlerinin ardından yolumuza devam edebiliriz. Bu amaçla, Visual Studio 2010 ortamında yeni bir proje oluşturup, Web sekmesinde yer alan WCF Rest Service Application şablonunu seçmemiz yeterli olacaktır. Bu şablonun kurulum işlemi sonrasında çıkmaması olasıdır. Bu durumda New Project iletişim kutusunda yer alan Enable the loading per-user extensions bağlantısını kullanarak etkinleştirme işlemini yapmamız yeterli olacaktır. Visual Studio 2010 ortamımızı tekrardan açtığımızda aşağıdaki ekran görüntüsünde olduğu gibi yeni proje şablonunun kullanabilir olduğunu göreceğiz.

HelloWebHttp isimli servis uygulamasına ait Solution içeriğinin ilk etapta otmatik olarak aşağıdaki gibi oluşturulduğunu gözlemleyebiliriz.

Service1.cs isimli örnek dosya içerisinde servis sözleşmesi(Service Contract) yer almaktadır. Bu sözleşme içerisinde yer alan metodlara WebGet ve WebInvoke niteliklerinin(Attributes) uygulandığı görülmektedir. Bu nitelikler bildiğiniz üzere servis operasyonlarına HTTP Post,Get,Put,Delete çağrılarının yapılabilmesi için gereklidir. Sınıf içerisine Get, Post, Put ve Delete metodlarının her biri için örnek nitelik kullanımları serpiştirilmiştir. Ayrıca serileştirilebilir örnek bir tipte yer almaktadır(SampleItem). GetCollection operasyonu SampleItem tipinden generic bir listeyi HTTP Get metoduna göre döndürmektedir. Create servis operasyonu ile yeni bir SampleItem nesnesinin HTTP Post metoduna göre örneklenmesi sağlanır. Tek bir SampleItem nesne örneğinin elde edilmesi için Get isimli servis operasyonunun aşırı yüklenmiş diğer bir versiyonu kullanılmaktadır. Update servis operasyonu ile HTTP Put metoduna göre güncelleme işlemi yapılmakta olup, Delete servis operasyonuda bir SampleItem nesnesinin HTTP Delete metoduna göre silinmesini sağlamaktadır. Solution içerisinde dikkat çekici bazı noktalar bulunmaktadır;

  • Örneğin web.config içeriği. Burada WCF 4.0 ile birlikte gelen basitleştirilmiş konfigurasyon(Simplified Configuration) özelliklerine yer verilmiştir. Bu sebepten daha sade, okunaklı ve fonksiyonel bir konfigurasyon içeriği oluşmuştur.
  • Dikkat çekici bir diğer noktada global.asax dosyasının var olmasıdır. Aslında bu bir web uygulaması olduğu için son derece normaldir. Lakin gözden kaçırılmaması gereken bir gerçek vardır; svc uzantılı bir servis dosyası fiziki olarak yoktur. Çünkü Asp.Net 4.0 Routing özelliği kullanılmaktadır. Çok tabi olarak yönlendirme işlemleri için global.asax dosyasında yapılması gereken bazı işlemler vardır. Bu nedenle hazır olarak global.asax içeriği aşağıdaki gibi üretilmektedir.

  • Yine servis kodlarına baktığımızda OperationContract niteliğinin kullanılmadığını görürüz. Oysaki .Net 3.5 ve WCF Rest Starter Kit sürümlerine baktığımızda WebGet ve WebInvoke nitelikleri dışında OperationContract niteliğininde kullanılması gerektiğini bilmekteyiz. OperationContract servis sözleşmelerinden sunulan operasyonların WCF çalışma zamanına bildirilmesinde rol oynadığı için bu son derece doğaldır. Ne varki WCF WebHttp servislerinde OperationContract niteliği opsiyoneldir.

Şimdi servis uygulamamız üzerinde bir kaç küçük değişiklik yapalım. Öncelikli olarak hayatımızı kolaylaştırmak adına Entity Framework' ten yararlanalım ve meşhur Chinook veritabanını ve işlemleri basit bir biçimde ele almak için sadece Artist tablosunu kullanmak istediğimizi düşünelim. Gerçi bu noktadan sonra biraz WCF Data Service' lere doğru kaymaya başlamış oluyoruz ancak amacımız tabiki HTTP Get,Post,Put ve Delete işlemlerini kendi kontrolümüz altında geliştirmek. 

Not : Tam bu noktada geliştirilen uygulamanın Data Service' ten veya RIA Service' ten ne farkı kaldığı sorusu akla gelebilir. Wink WCF WebHttp servislerinde asıl nokta operasyonun non-SOAP olacak şekilde sunulması(yani HTTP Get,Post,Put,Delete) ayrıca URI, format, protocol gibi bilgilerin tamamen geliştirici kontrolü altında olmasıdır.

Şimdi servis kodlarını aşağıdaki gibi geliştirdiğimizi varsayalım.

using System.Collections.Generic;
using System.Linq;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;

namespace HelloWebHttp
{
    [ServiceContract]
    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
    public class HelloService
    {
        [WebGet(UriTemplate = "ArtistList")]
        public List<Artist> GetAllArtists()
        {
            ChinookEntities entites = new ChinookEntities();
            return entites.Artist.OrderByDescending(a => a.Name).ToList();
        }

        [WebGet(UriTemplate = "Artist/{name}")]
        public List<Artist> FindArtists(string name)
        {
            ChinookEntities entities = new ChinookEntities();

            return (from artist in entities.Artist
                    where artist.Name.Contains(name)
                    select artist).ToList();
        }

        [WebGet(UriTemplate = "Artist/InRange?idFirst={firstId}&idSecond={secondId}")]
        public List<Artist> GetArtistsInRange(int firstId, int secondId)
        {
            ChinookEntities entities = new ChinookEntities();
            return (from a in entities.Artist
                   where a.ArtistId >= firstId && a.ArtistId <= secondId
                   select a).ToList();
        }
    }
}

Dikkat edileceği üzere sadece HTTP Get metodlarının çalışmasına yönelik 3 örnek operasyon yer almaktadır. WebGet niteliklerinde UriTemplate özelliklerine atanan değerler yardımıyla HTTP Get taleplerinin nasıl olması gerektiği belirlenmektedir. Servise gelen taleplerin Routing sürecine dahil olması için global.asax.cs kodlarında da aşağıdaki değişiklikleri yapmamız yeterli olacaktır.

using System;
using System.ServiceModel.Activation;
using System.Web;
using System.Web.Routing;

namespace HelloWebHttp
{
    public class Global
        : HttpApplication
    {
        void Application_Start(object sender, EventArgs e)
        {
            RegisterRoutes();
        }

        private void RegisterRoutes()
        {
            RouteTable.Routes.Add(new ServiceRoute("Chinook", new WebServiceHostFactory(), typeof(HelloService)));
        }
    }
}

Dikkat edileceği üzere http://makineadı:portnumarası/Chinook üzerine gelen talepler sonrasında, WebServiceHostFactory' nin ayağa kaldırılması ve HelloService sınıfının örneklenerek işleme alınmasının sağlanması gerçekleştirilmektedir. Artık testlerimize başlayabiliriz.

Not: Dilerseniz Web uygulamasını IIS üzerinden host ederekte deneyebilirsiniz. Ancak ister Asp.Net Development Server ister IIS olsun, URL satırında RegisterRoutes metodunda yer alan Chinook bilgisini kullanmamız servise ulaşmamız için yeterli olacaktır.

Tabi test derken ilk etapta servis operasyonlarını nasıl çağırabileceğimizi bilemeyebiliriz. Yada bulmak için araştırmaya üşenebiliriz. Embarassed İşte bu amaçla WCF tarafına gelen Auto Help yetenekleri sayesinde çalışma zamanında yardım sayfasına gidebilir ve servis operasyonlarını nasıl çağırabileceğimizi, içeriklerinin ne olacağını görebiliriz.. Aynen aşağıdaki ekran görüntüsünde olduğu gibi.

İlk olarak belirli bir kelimeyi içeren Artist listesini elde etmek istediğimizi düşünelim. Örneğin adında Milton kelimesi geçenleri bulmak istiyoruz. Bu durumda URL satırında Chinook/Artist/Milton yazmamız yeterli olacaktır. Sonuçlar aşağıdaki ekran görüntüsünde olduğu gibidir.

Eğer tüm Artist listesini elde etmek istiyorsak bu durumda URL satırından Chinook/ArtistList bilgisini girmemiz yeterli olacaktır. Bu durumda elde edilen sonuçlar aşağıdaki ekran görüntüsünde olduğu gibidir.

Son olarak ArtistId değer aralığına göre Artist listesini elde etmek istediğimizi düşünelim. Bu durumda URL satırından Chinook/Artist/InRange?idFirst=155&idSecond=158 gibi bir URL satırı girmemiz yeterli olacaktır ki buna göre örneğin ArtistId değerleri 155 ile 158 aralığında olanların listesini aşağıdaki ekran görüntüsünde olduğu gibi elde edebiliriz.

Oldukça basit ve etkili değil mi? WCF WebHttp Service' ler ile ilişkili incelemelerimize devam ediyor olacağız. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

HelloWebHttp.rar (31,23 kb) [Örnek Visual Studio 2010 Ultimate Beta 2 Sürümünde geliştirilmiş ancak RC sürümü üzerinde de test edilmiştir]