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

Ado.Net Data Services 1.5 CTP2 - Data Binding Bölüm 2

Pazartesi, 16 Kasım 2009 08:09 by bsenyurt

Merhaba Arkadaşlar,

Orta uzunlukta bir yazı için hazır mısınız? Analizi dikkat gerektiren bir Ado.Net Data Service örneği geliştiriyor olacağız. Genellikle bu tarz yazılara ait kodları geliştirirken sıkılmamak ve zihnimi açık tutmak için ya kahve içerim yada mutluluktan havalara uçarmış gibi yazabilmek ve kan şekerimi üst seviyede tutabilmek için bazen değişik şekerlemelerden yerim. Aynen yandaki resimde olduğu gibi.Cool 

Hatırlayacağınız gibi bir önceki yazımızda, Ado.Net Data Service için istemci taraflı veri bağlama işlemlerinde DataServiceCollection<T> kolekisyonunu değerlendirmeye çalışmış ve istemci tarafında bu konuyu ele almak için basit bir WPF uygulaması geliştirmiştik. Bir önceki örneğimiz aslında tek yönlü veri bağlama işlemine örnek olmasında rağmen, iki yönlü modeli de desteklemektedir. Bu yazımızda geliştireceğimiz örneğimizdeki hedefimiz ise, istemci tarafındaki koleksiyonlar üzerinden yapmış olduğumuz güncelleştirme, ekleme ve silme işlemlerini servis tarafına yansıtmaktır.

Aslında süreç son derece basittir. Veri bağlı kontroller üzerinde yapılan güncelleştirme hareketleri, DataServiceCollection koleksiyonu üzerinde de gerçeklenecektir. Sonrasında ise DataServiceContext türevli nesne örneği üzerinden SaveChanges metodunun çağırılması yeterli olacaktır. Bu sayede koleksiyon üzerinden yapılan tüm güncelleştirme, ekleme ve silme işlemlerinin, servis tarafına bir talep olarak gönderilmesi ve sunucu üzerinde de kullanılan veri kaynağına göre(Entity Framework veya Custom LINQ Provider) uygun bir veri işleminin yapılması sağlanır. Biz örneğimizde kendi geliştireceğimiz bir veritabanı içeriğini ve Ado.Net Entity Framework modelini kullanacağımızdan, sunucu üzerinde SQL sorgularının çalıştırıldığını göreceğiz. Dilerseniz vakit kaybetmeden örneğimizi geliştirmeye başlayalım ve işleyişini analiz edelim.

Örneğimizde kendi geliştirdiğimiz BookShop isimli bir veritabanını kullanıyor olacağız. Hayali olarak bir kitapçının veri ambarı olarak tasarladığımızı farz edebiliriz. Wink Söz konusu veritabanını oluşturmak için BookShopDbScripts.sql (13,83 kb) dosyasından yararlanabilirsiniz. Bu dosya içerisinde veritabanı, tabloların oluşturulması ve örnek veri girişleri için gerekli SQL Script' leri bulunmaktadır. Bu noktadan yola çıkarak geliştireceğimiz Ado.Net Data Service örneğinde kullanacağımız Entity DataModel diagramını aşağıdaki gibi tasarlayabiliriz. Örneğimizdeki amacımız kitap güncellemek, kitap eklemek ve silmek gibi işlemler olacaktır.

Gelelim Ado.Net Data Service örneğimize. Version 1.5 CTP2 versiyonuna göre eklediğimiz servisin kod içeriğini aşağıdaki gibi tasarlayabiliriz.

using System.Data.Services;

namespace BookShop
{
    public class BookService
        : DataService<BookShopEntities>
    {
        public static void InitializeService(DataServiceConfiguration config)
        {
            config.SetEntitySetAccessRule("*", EntitySetRights.All);        
            config.DataServiceBehavior.MaxProtocolVersion = System.Data.Services.Common.DataServiceProtocolVersion.V2;
     }
    }
}

İstemci tarafını yine bir WPF uygulaması olarak tasarlayıp, iki yönlü veri bağlama kabiliyetlerini etkin bir şekilde kullanmayı planlıyoruz. Yazıyı hazırladığım sıralarda, Visual Studio 2008 üzerinde garip ve gizemli bir sorunla karşılaştım. Öyleki, istemci uygulamada Add Service Reference ile proxy üretimi yapılmasına rağmen, indirilen Entity tiplerinin INotifyPropertyChanged arayüzünü uygulamadıklarını gördüm. Ama tabiki çaresiz değildim. DataSvcUtil aracının version 1.5 CTP 2 sürümünü aşağıda görüldüğü gibi kullanarak, istemci için gerekli olan ve INotifyPropertyChanged arayüzünü implemente etmiş tipleri üretebiliriz.

Bu noktadan sonra üretilen sınıfı istemci tarafında kullanmamız yeterli olacaktır. İşte istemci tarafı için üretilen Entity tipleri.

Yanlız komut satırından yapılan proxy sınıfını istemcide kullanabilmek için, Ado.Net Data Services Version 1.5 CTP2 ile gelen Microsoft.Data.Services.Client assembly' ının projeye referans edilmesi gerekmektedir. Aynen aşağıdaki ekran görüntüsünde olduğu gibi.

Bu ön hazırlıklardan sonra istemci tarafındaki Window1 içeriğini başlangıçta aşağıdaki gibi tasarladığımızı düşünelim.

Burada üst tarafta yer alan ComboBox içerisinde kitap kategorileri, alt tarafta yer alan ListBox kontrolünde ise seçilen kategoriye bağlı kitaplar görüntülenecektir. Yanlız XAML tarafına baktığınızda WPF açısından etkili bazı tekniklerin kullanıldığını görebilirsiniz.

XAML İçeriği;

<Window x:Class="BookSeller.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Book Seller" Height="330" Width="384">
    <Grid Name="grdBook" Height="298">
        <ComboBox Height="42" Margin="2,12,0,0" Name="cmbCategories" VerticalAlignment="Top"  IsSynchronizedWithCurrentItem="true" ItemsSource="{Binding}">
            <ComboBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Name="txtCategoryId" Text="{Binding Path=CategoryId}" FontSize="22" Foreground="RosyBrown"/>
                        <TextBlock Name="txtCategoryName" Text="{Binding Path=Name}"/>
                    </StackPanel>
                </DataTemplate>
            </ComboBox.ItemTemplate>
        </ComboBox>
        <ListBox IsSynchronizedWithCurrentItem="True" Name="lstProducts" Margin="0,60,0,53" ItemsSource="{Binding Book}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Vertical">
                        <TextBox Name="txtProductName" Text="{Binding Name}" Width="250"/>
                        <TextBox Name="txtListPrice" Text="{Binding ListPrice}" Width="100" Foreground="Gold" Background="Black" HorizontalAlignment="Right"/>
                    </StackPanel>                   
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
        <Button Height="23" HorizontalAlignment="Left" Margin="2,0,0,24" Name="btnSaveChanges" VerticalAlignment="Bottom" Width="93" Click="btnSaveChanges_Click">Save Changes</Button>
    </Grid>
</Window>

Her şeyden önce, ComboBox içeriğinin her bir öğesinin birer StackPanel olduğunu ve CategoryId ile Name alanlarının içeriklerinin TextBlock' ların Text özelliklerine bağlandıklarını görebiliriz. Benzer durum ListBox kontrolü içinde geçerlidir. Ne varki ListBox kontrolünün içerisinde yer alan ve Text özellikleri Book Entity' sinin Name ile ListPrice alanlarına bağlanmış olan TextBox kontrolleri, aslında kullanıcı tarafından düzenlenebilirde. Laughing Volaaaa!!! Öyleyse akla şu gelebilir.

"Eğer bu kontrollerin Text özellikleri, kod tarafındaki Entity örneklerine bağlanmışlarsa ve çalışma zamanında içerikleri değiştirilirse bu düzenlemeler Entity içeriklerine de yansır mı? Peki diyelim ki yansıdı. SaveChanges metodunu çağırdığımızda bu değişikliler servis tarafına da yansır mı?"

Aslında bu sorularının tamamının cevabı Evet' tir. Ama tabiki bizim bu durumu analiz etmemiz ve gözümüzle görmemiz şart. Wink Buna göre kod içeriğimizi aşağıdaki gibi geliştirmemiz yeterlidir.

using System;
using System.Data.Services.Client;
using System.Windows;
using BookShopModel;

namespace BookSeller
{
    public partial class Window1
           : Window
    {
        BookShopEntities bs = null;
        DataServiceCollection<Category> _bookShopCollection = null;

        public Window1()
        {  
            InitializeComponent();

            bs = new BookShopEntities(new Uri("http://localhost:7995/BookService.svc/"));
            _bookShopCollection = DataServiceCollection.CreateTracked<Category>(bs, bs.Category.Expand("Book"));
            grdBook.DataContext = _bookShopCollection;
        }

        private void btnSaveChanges_Click(object sender, RoutedEventArgs e)
        {
            bs.SaveChanges();
        }
    }
}

Programı ilk çalıştırdığımızda aşağıdaki bilgilerin geldiğini görebiliriz. Her ne kadar tasarım konusunda zayıf bir örnek olsada, ListBox içerisinde her bir satırda düzenlenebilir, veriye bağlanmış kontrollerin yer alması dahi benim için önemli bir adımdır. Smile

Şimdi ListBox içerisindeki verilerde değişiklik yapıldığını varsayalım. Örnek olarak Pro WCF 3.5 isimli kitabın adına Second Edition kelimelerini ilave ettiğimizi ve ListPrice değerini 39 birim olarak değiştirdiğimizi düşünebiliriz. Aynen aşağıdaki ekran görüntüsünde olduğu gibi.

Şimdi Save Changes başlıklı düğmeye basalım ve BookShopEntities nesne örneği üzerinden SaveChanges metodunun çağırıldığı yerde koyduğumuz break point üzerinde duralım. Yapamamız gereken, BookShopEntities ve DataServiceCollection içeriklerini Watch ile incelemek olacaktır. İlk olarak _bookShopCollection içeriğine bir bakalım.

Hımmmmm... Wink Görüldüğü üzere Category üzerinden gittiğimiz Book özelliğine bağlı koleksiyonda az önce yapılan Name ve ListPrice değişikliklerin gerçekleştirildiği gözlemlenmektedir. Benzer şekilde BookShopEntities nesne örneğinin içeriğine baktığımızda, ilgili Book örneği için aynı değişikliklerin yansıtıldığını görebiliriz.

Dolayısıyla iki yönlü veri bağlama(Two Way DataBinding) nedeniyle kontroller üzerine yansıyan verilerde yapılan değişiklikler, istemci tarafındaki ilgili koleksiyonlara da yansıtılmaktadır. Buna göre SaveChanges metoduna yapılan çağrı geçildiğinde, servis tarafına gerekli güncelleştirme talebinin gittiği ve aşağıdaki SQL sorgusunun çalıştırıldığı gözlemlenir.

exec sp_executesql N'update [dbo].[Book]
set [Name] = @0, [ListPrice] = @1, [PageSize] = @2
where ([BookId] = @3)
',N'@0 nvarchar(26),@1 decimal(19,4),@2 smallint,@3 int',@0=N'Pro WCF 3.5 Second Edition',@1=39.0000,@2=500,@3=2

Eğer birden fazla Book örneğinin özelliği değiştirilirse, SaveChanges metodu çağrısı sonrasında her bir güncelleştirme için ayrı bir SQL Update sorgusunun çalıştırıldığı da gözlemlenecektir. Şimdi örnek bir veri ekleme işlemi yapalım. Bu amaçla Window1 içeriğini aşağıdaki gibi değiştirdiğimizi düşünebiliriz.

Yapılan bu değişikliklerden sonra ise XAML içeriğinin aşağıdaki gibi oluştuğu gözlemlenebilir.

<Window x:Class="BookSeller.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Book Seller" Height="362" Width="384">
    <Grid Name="grdBook" Height="317">
        <ComboBox Height="42" Margin="2,12,0,0" Name="cmbCategories" VerticalAlignment="Top"  IsSynchronizedWithCurrentItem="true" ItemsSource="{Binding}">
            <ComboBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Name="txtCategoryId" Text="{Binding Path=CategoryId}" FontSize="22" Foreground="RosyBrown"/>
                        <TextBlock Name="txtCategoryName" Text="{Binding Path=Name}"/>
                    </StackPanel>
                </DataTemplate>
            </ComboBox.ItemTemplate>
        </ComboBox>
        <ListBox IsSynchronizedWithCurrentItem="True" Name="lstProducts" Margin="0,60,0,141" ItemsSource="{Binding Book}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Vertical">
                        <TextBox Name="txtProductName" Text="{Binding Name}" Width="250"/>
                        <TextBox Name="txtListPrice" Text="{Binding ListPrice}" Width="100" Foreground="Gold" Background="Black" HorizontalAlignment="Right"/>
                    </StackPanel>                   
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
        <Button Height="23" HorizontalAlignment="Right" Margin="0,0,12,12" Name="btnSaveChanges" VerticalAlignment="Bottom" Width="93" Click="btnSaveChanges_Click">Save Changes</Button>
        <Label Height="28" HorizontalAlignment="Left" Margin="7,0,0,106" Name="label1" VerticalAlignment="Bottom" Width="54">Name</Label>
        <Label Height="28" HorizontalAlignment="Left" Margin="7,0,0,77" Name="label2" VerticalAlignment="Bottom" Width="54">ListPrice</Label>
        <Label Height="28" HorizontalAlignment="Right" Margin="0,0,104,78" Name="label3" VerticalAlignment="Bottom" Width="65">Page Size</Label>
        <TextBox Height="23" Margin="67,0,12,112" Name="txtName" VerticalAlignment="Bottom" />
        <TextBox Height="23" Margin="67,0,0,83" Name="txtListPrice" VerticalAlignment="Bottom" HorizontalAlignment="Left" Width="97" />
        <TextBox Height="23" Margin="0,0,12,83" Name="txtPageSize" VerticalAlignment="Bottom" HorizontalAlignment="Right" Width="97" />
        <Button Height="23" HorizontalAlignment="Left" Margin="12,0,0,48" Name="btnAddBook" VerticalAlignment="Bottom" Width="93" Click="btnAddBook_Click">Add</Button>
    </Grid>
</Window>

Kullanıcı bu forma göre yeni bir kitap ekleyebilmelidir. Bir kitabın bir kategori altında olması gerektiğinden, oluşturulacak kitabın hangi kategoriye ekleneceğinin belirlenmesi sırasında ComboBox' ta seçili olan Category nesne örneğinden yararlanabiliriz. Kitabın tabiki öncelikle nesnel olarak oluşturulması gerekmektedir. Sonrasında ise ComboBox' ta seçili olan Category öğesinin Book özelliği yardımıyla ilgili koleksiyona eklenmelidir. Bu ekleme işleminin ardından yapılacak olan SaveChanges çağrısı, ekleme işlemi için Ado.Net Data Service tarafına uygun talebin gönderilmesini sağlayacaktır. Bunun doğal sonucu olarakta sunucu tarafında uygun olan SQL Insert sorgusu çalıştırılacaktır. Bakalım gerçekten böyle mi? Wink Bu amaçla Add başlıklı Button kontrolümüzün Click olay metodunu aşağıdaki gibi kodladığımızı düşünelim.

private void btnAddBook_Click(object sender, RoutedEventArgs e)
        {
            Category currentCategory = cmbCategories.SelectedItem as Category;

            Book newBook = new Book
            {
                ListPrice = Convert.ToDecimal(txtListPrice.Text),
                Name = txtName.Text,
                PageSize = Convert.ToInt16(txtPageSize.Text) ,
                Category=currentCategory
            };

            currentCategory.Book.Add(newBook);
        }

İlk olarak kitabın ekleneceği kategori bulunmaktadır. Burada SelectedItem özelliğinin Category tipine dönüştürüldüğüne dikkat edilmelidir. Sonrasında yeni bir Book nesnesi örneklenir ve ilgili özellikleri kontrollerden alınır.(Burada herhangibir hatalı giriş kontrolü yapmadığımızı belirtelim. Aslında yapmamız gerekiyor ancak şu an için odaklanmamız gereken kısım bu değil. Yinede siz örneği denerken mutlaka olası hataların önüne geçmenizi sağlayacak eklemeleri yapmayı unutmayın Wink ) Dikkat edilmesi gereken noktalardan biriside, Book nesnesi örneklenirken Category özelliğine currentCategory değişkeninin referansını atamamızdır. Bundan sonraki kısım ise son derece basittir. Örneklenen Book nesne örneği, o an seçili olan Category nesnesinin Book özelliğinin temsil ettiği koleksiyona atanır. Birde çalışma zamanına bakalım. Aşağıdaki örnekte bir veri girişi yapılmak istendiğini görüyoruz.

Şimdi Add düğmesine basarsak ve kodu debug edersek currentCategory değişkeninin içeriğinin aşağıdaki gibi güncellendiğini görürüz.

Görüldüğü gibi yeni Book nesne örneği ilgili kategori altına eklenmiştir. Öyleyse birde DataServiceCollection içeriğimize bakalım.

Yeni eklenen Book nesne örneğinin DataServiceCollection nesne örneği içerisinde de yer aldığı gözlemlenmektedir. Üstelik çalışma zamanının yeni görüntüsüde aşağıdaki gibidir.

Bağlılık buna denir desek yeridir Cool Nitekim yapmış olduğumuz nesne eklemesinden ListBox kontrolüde otomatik olarak etkilenmiş ve içeriğini yenilemiştir. Artık Save Changes başlıklı düğmeye basarak değişiklikleri servis tarafına gönderebiliriz. Bu işlem yapıldığı takdirde SQL tarafında aşağıdaki sorgunun çalıştırıldığı gözlemlenir.

exec sp_executesql N'insert [dbo].[Book]([Name], [ListPrice], [CategoryId], [PageSize])
values (@0, @1, @2, @3)
select [BookId]
from [dbo].[Book]
where @@ROWCOUNT > 0 and [BookId] = scope_identity()',N'@0 nvarchar(28),@1 decimal(19,4),@2 int,@3 smallint',@0=N'Yazılımcılar için SQL Server',@1=45.0000,@2=1,@3=800

İşte bu kadar...Sırada silme işlemi var ama uzun olan bir yazının verdiği yorgunluğu yaşayan ben bu kutsal görevi siz değerli okurlarıma bırakıyorum Wink Silme operasyonunu uygularken debug işlemlerini yapmayı ve çalışma zamanını analiz edip SQL tarafında neler olup bittiğini incelemeyi unutmayın. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

BindingV2.rar (93,31 kb)

Ado.Net Data Services 1.5 CTP2 - Data Binding Bölüm 1

Pazartesi, 9 Kasım 2009 03:30 by bsenyurt

Merhaba Arkadaşlar,

Ado.Net Data Services ile geliştirilen servislerin tüketilmesi sırasında önem arz eden konulardan biriside, istemci tarafındaki veri bağlama(DataBinding) işlemleridir. Öyleki, servisin tüketicisi olan istemcilerin

  • Veriye bağlanılması,
  • Bağlanılan verilerin ilgili kontrollerde gösterilmesi,
  • Kontroller üzerinden yapılan değişikliklerin aslında bağlanılan Entity içeriklerinde de gerçekleştirilmesi,
  • İstemci tarafındaki veri içeriğindeki değişimlerin servis tarafına da gönderilmesi(SaveChanges çağrısı sonrası)

gibi fonksiyonellikleri desteklemesi gerekir. Ancak istmeci tarafında WPF(Windows Presentation Foundation) veya Silverlight gibi zengin içerik sağlayan uygulamalar söz konusu olduğunda, bu modellerin getirdiği veri bağlama kolaylıklarından da yararlanılmalıdır.

Ado.Net Data Services v1.5 ile birlikte istemci tarafına getirilen DataServiceCollection isimli koleksiyonun veri bağlama işlemlerinde kullanılabilmekte olup, CTP2 versiyonunda dahada iyileştirilmiş olarak karşımıza çıkmaktadır. Buna göre istemci tarafı için üretilen kütüphanede(Client Library) kolaylaştırıcı değişiklikler yapıldığı söylenebilir. DataServiceCollection<T> koleksiyonu ObservabelCollection<T> tipinden türemekte olup, INotifyPropertyChanged ve INotifyCollectionChanged arayüzlerini(Interface) uygulamaktadır. Aşağıdaki Object Browser çıktısında bu tipin içeriği açık bir şekilde görülmektedir.

Bu nedenle WPF ve Silverlight tarafında ele alınanveri bağlama(DataBinding) ihtiyaçlarını hem one-way hemde two-way olarak karşılayabilecek bir koleksiyon olarak düşünülmelidir. Buna göre WPF ve Silverlight tarafındaki veri bağlı kontrollerin, DataServiceCollection sınıfından örneklenen koleksiyonları kullanabilmeleri mümkündür. Tahmin edileceği üzere iki yönlü bağlama sayesinde istemci Context' i ile Entity' ler arasındaki iletişimde, değişikliklerin karşılıklı olarak yansıtılabilmesi otomatikleştirilmektedir. Bir DataServiceCollection basit olarak, Ado.Net Data Service tarafına yapılacak bir çağrı ile kolayca doldurulabilir. Özellikle CTP2' de istemci tarafında DataServiceCollection örneklerinin daha güçlü bir şekilde ele alınması için gerekli iyileştirmelerin yapıldığı görülmektedir.

Dilerseniz Ado.Net Data Service hizmetlerinin kullanıldığı senaryolarda, veri bağlama işlemlerinin nasıl yapılacağını örnekler üzerinden incelemeye başlayalım. İlk olarak tek yönlü (One Way) sonrasında ise iki yönlü(Two Way) veri bağlama işlemlerini ele alıyor olacağız. İşe ilk olarak basit bir Asp.Net Web Application projesi oluşturarak başlayabiliriz. Projemizde Ado.Net Entity Framework tabanlı bir veri kaynağı kullanıyor olacağız. İstemci tarafında one-to-many ilişki içerisinde değerlendirilebilecek tipleri ele almak istediğimizden kobay olarak AdventureWorks veritabanındaki ProductSubcategory ve Product tablolarını kullanıyor olacağız. Wink Ado.Net Entitiy Diagramımız aşağıda görüldüğü gibidir.

Ado.Net Data Service öğesini projeye ekledikten sonra, ProductionService.svc' ye ait kod içeriğini aşağıdaki gibi düzenleyebiliriz.

using System.Data.Services;

namespace AdventureServices
{
    public class ProductionService
        : DataService<AdventureWorksEntities>
    {
        public static void InitializeService(DataServiceConfiguration config)
        {
            config.SetEntitySetAccessRule("*", EntitySetRights.All);
            config.SetServiceOperationAccessRule("*", ServiceOperationRights.All);
       
            config.DataServiceBehavior.MaxProtocolVersion = System.Data.Services.Common.DataServiceProtocolVersion.V2;
     }
    }
}

Önemli olan noktalarda birisi versiyon olarak CTP2' nin kullanılacağının DataServiceProtocolVersion.V2 ile belirtilmesidir.

Artık istemci tarafını tasarlamaya başlayabiliriz. Ancak öncesinde istemci tarafının Bağlayıcı Arayüzlerini(Binding Interfaces) uygulayabilmesi için komut satırından(Visual Studio 2008 Command Prompt) kod üretici ile ilgili bazı işlemlerin yapılması gerekmektedir. Visual Studio tarafından ele alanın ilgili kod üretim(Code Generation) kütüphanesine bu uygulama işini bildirmek için aşağıdaki komutlarım çalıştırılması yeterli olacaktır.

Bu kez gerçekten istemci tarafını yazmaya başlayabiliriz.  Smile Bu amaçla basit bir WPF uygulaması geliştireceğiz. (Size tavsiyem aynı örneği Silverlight üzerinde geliştirmeye çalışmanız olacaktır.) Uygulamamızı oluşturduktan sonra ilk yapacağımız işlem, Ado.Net Data Service' imize ait URL adresinden gerekli servis referansının, Add Service Reference yardımıyla projeye eklenmesi olacaktır.

Görüldüğü üzere, servis üzerinden sunduğumuz Product ve ProductSubcategory Entity içerikleri buraya yansıtılmaktadır. Referansın eklenmesinden sonra istemci tarafında aşağıdaki sınıf diagramında görülen tiplerin oluşturulduğu fark edilebilir.

Dikkat edileceği üzere Product ve ProductSubcategory tiplerine INotifyPropertyChanged arayüzü uygulanmıştır. Buna göre söz konusu tiplere ait özelliklerde olacak değişimler, örneklerin bağlandığı ortamlara otomatik olarak bildirilecektir. Elbette tam tersi durumda geçerlidir. Yine servis referansının eklenmesi sonrası, istemci tarafına Microsoft.Data.Services.Client assembly' ınında bildirildiği görülebilir ki bu assembly DataServiceCollection<T> gibi önemli tipleri içermektedir.

WPF uygulamamızın Window1 içeriğini görsel olarak aşağıdaki gibi tasarladığımızı düşünelim.

Görsel içeriğimizde yer alan ComboBox bileşenini ProductSubcategory içeriği ile dolduracağız. Diğer yandan alt tarafta görülen GridView kontrolünde, ComboBox bileşeninden seçilen alt kategoriye bağlı ürünlerin(dolayısıyla Product nesne örneklerinin) bazı özellikleri gösterilecektir. Çok doğal olarak ComboBox ve GridView bileşenlerinin, Data Service tarafından gelen içeriğe bağlanmaları gerekmektedir. Üstelik ProductSubcategory ve Product entity' leri arasında bire çok ilişki söz konusu olduğundan, ComboBox kontrolünde bir öğeden diğerine geçildiğinde, buna bağlı ürünlerinde GridView kontrolünde gösterilmesi sağlanmalıdır. Bu durumlar göz önüne alındığında söz konusu XAML içeriğini aşağıdaki gibi tasarlamamız yeterli olacaktır.

<Window x:Class="ClientApp.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Production Space" Height="300" Width="401">
    <Grid x:Name="grdProduction">
        <ComboBox ItemsSource="{Binding}" Height="23" Margin="0,33,0,0" Name="cmbSubCategories" VerticalAlignment="Top" IsSynchronizedWithCurrentItem="True">
            <ComboBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Path=Name}"/>
                </DataTemplate>
            </ComboBox.ItemTemplate>
        </ComboBox>
        <Label Height="28" HorizontalAlignment="Left" Margin="0,-1,0,0" Name="label1" VerticalAlignment="Top" Width="136">Product Sub Categories</Label>
        <Label Height="28" HorizontalAlignment="Left" Margin="0,71,0,0" Name="label2" VerticalAlignment="Top" Width="136">Products</Label>
        <ListView ItemsSource="{Binding Product}" Margin="0,96,0,0">
            <ListView.View>
                <GridView>
                    <GridViewColumn Header="Id" DisplayMemberBinding="{Binding Path=ProductID}"/>
                    <GridViewColumn Header="Name" DisplayMemberBinding="{Binding Path=Name}"/>
                    <GridViewColumn Header="List Price" DisplayMemberBinding="{Binding Path=ListPrice}"/>
                    <GridViewColumn Header="Class" DisplayMemberBinding="{Binding Path=Class}"/>
                    <GridViewColumn Header="Number" DisplayMemberBinding="{Binding Path=ProductNumber}"/>
                </GridView>
            </ListView.View>
        </ListView>
    </Grid>
</Window>

ComboBox kontrolünün ItemsSource özelliğinin Binding olarak bırakıldığına dikkat edelim. Buna göre grdProduction isimli Grid kontrolünün DataContext kaynağı ne ise, ComboBox kontrolü bu kaynağa otomatik olarak bağlanacak ve her bir öğesinde, Name alanının değerini({Binding Path=Name} den dolayı) gösterecektir. Dolayısıyla kod tarafında Grid kontrolünün DataContext özelliğine atanan veri kümesi önem arz etmektedir. Diğer yandan ComboBox kontrolünün IsSynchronizedWithCurrentItem niteliğine true değeri atandığına da dikkat edilmelidir. Bu sayede ComboBox içerisinde olan değişiklikler, diğer veri bağlı kontrollerede iletilecektir. Yani GridView kontrolünün alt kategoriye bağlı ürünler ile doldurulması işleminin gerçekleştirilebilmesi için söz konusu niteliğin true değerine sahip olması gerekmektedir. ListView bileşeninin ItemsSource özelliğine {Binding Product} değeri atanmıştır. Buna göre, ListView içerisindeki veri bağlı kontrollerin Product kaynağına bağlanabileceği belirtilmiş olur ki bu sayede GridView kontrolünün GridViewColumn elementlerinin DisplayMemberBinding niteliklerine Product tipine ait özelliklerin adları atanmıştır. Peki tüm bu veri bağlı kontrollerin baz alacağı veri kaynağı nerede atanacaktır?

Bu amaçla kod tarafında aşağıdaki işlemleri yapmamız yeterli olacaktır.

Window1 Code içeriği;

using System;
using System.Data.Services.Client;
using System.Windows;
using ClientApp.ProductionSpace;

namespace ClientApp
{
    public partial class Window1
        : Window
    {
        AdventureWorksEntities adw = new AdventureWorksEntities(new Uri("http://localhost:1757/ProductionService.svc/"));

        public Window1()
        {
            InitializeComponent();

            grdProduction.DataContext = DataServiceCollection
                .CreateTracked<ProductSubcategory>(adw, adw.ProductSubcategory.Expand("Product"));
        }
    }
}

DataServiceContext türevli nesne örneği oluşturulduktan sonra Window1 yapıcı metodu(Constructor) içerisinde DataServiceCollection üzerinden CreateTracked isimli bir çağrı yapıldığı görülmektedir. Bu çağrıda ProductSubcategory tipinden bir nesne kümesinin listesi alınmaktadır. Ayrıca metodun ikinci parametresine dikkat edilecek olursa, her bir ProductSubcategory için Product genişletmesinin yapıldığı, yani alt kategoriye bağlı olan ürünlerinde talep edildiği görülmektedir. Uygulamanın çalıştırılması sonrasında CreateTracked metodunun çağırıldığı yerde Breakpoint ile ilerlenir ve SQL Server Profiler aracından arka planda çalıştırılan sorgu incelenirse aşağıdaki ifadenin yürütüldüğü görülebilir.

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

Çok fazla alan var değil mi? Undecided Her neyse...Gelelim çalışma zamanındaki duruma. Örneğin Handlebase alt kategorisini seçtiğimizde aşağıdaki şekilde görülen durum oluşmaktadır.

Başka bir alt kategori seçtiğimizde ise(örneğin Bottom Brackets) buna bağlı ürünlerin GridView kontrolüne doldurulduğu görülecektir.

Tabiki burada sadece entity içeriklerinin doldurulması ve veri kontrollerine tek yönlü(One-Way) bağlanması söz konusudur. Ancak tahmin edileceği üzere birde kontroller üzerinden verilerde yapılan değişiklikler sonrası bunların Entity içeriklerine yansıtılması ve sonrasında SaveChanges metodu ile tüm değişikliklerin servis tarafına gönderilmesi söz konusu olabilir ki buda iki yönlü(Two Way) bağlamanın tesis edilmesi ile kolayca gerçekleştirilebilir. Nitekim two-way binding metoduna göre, kolekisyonda olacak değişimler, SaveChanges metoduna yapılan çağrı sonucu servis tarafına ve dolayısıyla sunucu üzerindeki veri kaynağına da iletilecekteir. Bu konuyu bir sonraki yazımızda ele alıyor olacağız. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

DataBinding.rar (116,66 kb)

Screencast - Ado.Net Data Services 1.5 - Paging

Cuma, 6 Kasım 2009 09:00 by bsenyurt

Merhaba Arkadaşlar,

Ado.Net Data Services 1.5 CTP2 ile birlikte gelen yeniliklerden biriside sunucu tarafındaki verilerin sayfalanarak(Paging) gönderilebilmesidir. Asp.Net Web uygulamalarında sıklıkla kullandığımız sayfalama tekniğinin bir benzeri olarak düşünüldüğünde, istemci ve sunucu tarafında belirgin performans kazanımlarına neden olan bir özelliktir. Nitekim büyük çaplı verilerin bir bütün halinde ve hemen her istemci talebi sonrasında ilgili veri kaynağından(Entity Framework ve Custom LINQ Provider üzerinden) çekilmesi hem sunucu tarafında fazladan iş yüküne neden olmakta hemde istemci tarafına çok büyük boyutta veri akmasına neden olmaktadır. Kullanımı son derece kolay olan bu özelliği incelediğimiz görsel dersimizde SQL Profiler aracından da yararlanarak arka planda çalıştırılan sorguları analiz etme şansına da sahip olacağız. İyi seyirler Smile

   

Yeni görsel derslerde görüşmek dileğiyle hepinize mutlu günler dilerim.

Ado.Net Data Services 1.5 CTP2 - Web Friendly Feeds

Cumartesi, 31 Ekim 2009 20:25 by bsenyurt

Merhaba Arkadaşlar,

Ado.Net Data Services v1.5 CTP1 ile gelen Web Friendly Feeds özelliği, CTP2 sürümünde eklenen iki yeni eşleştirme seçeneği ile genişletilmiştir. Durun bir dakika...Web Friendly Feeds nedir? Undecided Arkadaşlıktan farklı bir şey olsa gerek Wink Öncelikle bu konuya açıklık getirmek gerekiyor.

Web Friendly Feeds özelliği, bir Entity'nin herhangibir özelliğini(Property), Ado.Net Data Service' inden çıktı olarak üretilen Atom içeriğindeki bir elemente eşleştirmekte kullanılmaktadır. Nitekim servisin ürettiği varsayılan Atom içeriğinde yer alan author name, url, title vs... gibi bilgiler zaten standart olarak kabul edilmiştir ve bu nedenle söz konusu elementleri değerlendiren yorumlayıcılara, var olan Entity içeriğindeki bazı özellik değerlerinin aktarılması istenebilir. Bir başka deyişle, servisin ürettiği içeriğin kaynağındaki özelliklerin çıktıda map edileceği yerler, Atom içeriğindeki belirli noktalar olarak belirlenebilir.

Çok doğal olarak eşleştirmenin çalışma zamanında değerlendirilmesi gerekmektedir. Ado.Net Data Servislerin, arka planda Entity Framework veya Custom LINQ Provider kullandığı düşünüldüğünde, eşleştirme işleminin nerede bildirileceği şu anda bizim için öğrenilmesi gereken yegane noktadır. Bir geliştirici olarak söz konusu eşleştirme bildirimlerinin nitelik(attribute) veya konfigurasyon bazlı olarak yapılacağının düşünülmesi son derece doğaldır ki bunların çalışma zamanında ele alınması gerekmektedir. Biz bu yazımızda önce eksikliği anlamaya çalışacak, sonrasında ise Entity Framework kullanılması halinde eşleştirmeyi nasıl gerçekleştirebileceğimizi inceleyeceğiz.

Öncelikle basit bir Asp.Net Web uygulaması oluşturalım ve aşağıdaki EDM grafiğinde görülen Ado.Net Entity Data Model öğesini söz konusu projeye ekleyelim. Her zamanki gibi kobay olarak AdventureWorks veritabanını hedef alıyor olacağız.

Contact tablosunu değerlendirmek amacıyla basit bir Ado.Net Data Services v1.5 CTP2 öğesini projeye ekleyerek devam edelim. Öğemizin kod içeriğini aşağıdaki gibi geliştirmemiz konumuz için yeterlidir.

using System.Data.Services;

namespace WebFriendlyFeed
{
    public class AdventureServices
        : DataService<AdventureWorksEntities>
    {
        public static void InitializeService(DataServiceConfiguration config)
        {          
            config.SetEntitySetAccessRule("*", EntitySetRights.AllRead);       
            config.DataServiceBehavior.MaxProtocolVersion = System.Data.Services.Common.DataServiceProtocolVersion.V2;
     }
    }
}

Söz konusu servis üzerinden Contact tablosundaki örneğin ilk 3 satırı talep ettiğimizde aşağıdakine benzer bir çıktı elde ederiz.

Eksiklik şudur; standart atom feed içeriğinde yer alan author/name ve title elementlerinin içeriği boştur. Bu bilgilerin eksik olması şu aşamada önemli değilmiş gibi görünebilir. Ama bu atom feed çıktısını değerlendiren bir uygulamada söz konusu alanlar belirli amaçlar ile kullanılıyor olabilir ve bu nedenden boş olmaları yararlı olmayabilir. Örneğin Internet Explorer' ın feed içeriklerini görsel olarak yorumlama özelliğini(Turn on feed reading view) açtığımızda aynı sorgu aşağıdaki sonucu üretecektir.

Görüldüğü gibi ilk etapta feed entry' leri ile ilişkili title veya author/name gibi elementler doldurulmadığı için okunabilir bir içeriğin oluşmadığı görülmektedir. Üstelik Title gibi alanlara göre sıralama işlemi yapılmasıda mümkün değildir. İşte Ado.Net Data Services 1.5 sürümünde getirilen Web Friendly Feed özelliği, söz konusu standart entry elementlerinden bazılarının, entity içeriğinden beslenmesini sağlayabilmektedir. Peki ama nasıl? Undecided Örneğimizde Entity Framework modeli kullanıldığından, Conceptual Schema Definition Language(CSDL) içeriğinde bazı ayarlamalar yapılması gerekmektedir. (Tabi Entity modelinin Update edilmesi halinde bu değişikliklerin uçabileceğini hatırlatmam gerekir.) Aşağıdaki şekilde görüldüğü gibi.

İlk olarak bir namespace ilave edildiğini görüyoruz. Bu namespace ilave edilmez ise, FC_KeepInContent veya FC_TargetPath gibi nitelikleri kullanamayız. Yeri gelmişken bu niteliklerin ne iş yaptıklarını açıklayalım. CSDL içeriğinde yer alan FirstName özelliğinin değerinin Atom Feed içerisindeki author/name elementinde çıkması için FC_TargetPath niteliğine SyndicationAuthorName değeri atanmıştır. Benzer şekilde Atom Feed içeriğinin bir Contact için üretilen Title elementinde LastName özelliğinin değerinin çıkması için, FC_TargetPath niteliğine SyndicationTitle değeri atanmıştır. Aynı işlem Email adresi içinde gerçekleştirilmiş ve özellik değerinin author/email elementinde çıkması için FC_TargetPath niteliğine SyndicationAuthorEmail değeri atanmıştır. Buna göre FC_TargetPath niteliğinin değerinin, entity özelliğinin hangi atom alanında çıkacağını belirttiğini ifade edebiliriz. FC_KeepInContent niteliğine atanan false değeri ilede, atom feed' in element noktlarında çıkan değerlerin, üretilen Contact elementine ait content içeriğinde gösterilmemesi sağlanmaktadır. Buna göre biraz önce yaptığımız talebi tekrarlarsak aşağıdaki çıktıları alırız.

Ve atom çıktısını içeriği;

Görüldüğü üzere FirstName ve EmailAddress bilgileri, author elementi altındaki name ve email alt elementlerine yazılmıştır. Ayrıca entry' nin title elementinin içeriğinede LastName özelliğinin değerinin yazıldığı görülmektedir. Bununla birlikte buradaki özellikler, entry elementi altındaki content elementi içeriğindende çıkartılmıştır. Süper Smile Peki Atom Feed Entry Elementlerinden hangilerine atamalar yapabiliriz? İşte Ado.Net Data Services v1.5 CTP2 eklentilerinin de yer aldığı son liste. 

Entity tip özelliklerini hangi Atom Entry elementlerine eşleştirebiliriz.

author/email -> SyndicationItemProperty.AuthorEmail
author/name -> SyndicationItemProperty.AuthorName
author/uri  -> SyndicationItemProperty.AuthorUri
published  -> SyndicationItemProperty.Published
rights  -> SyndicationItemProperty.Rights
summary -> SyndicationItemProperty.Summary (CTP2 ile Gelmiştir)
title  -> SyndicationItemProperty.Title
Updated -> SyndicationItemProperty.Updated (CTP2 ile Gelmiştir)
contributor/name -> SyndicationItemProperty.ContributorName
contributor/email -> SyndicationItemProperty.ContributorEmail
contributor/uri -> SyndicationItemProperty.ContributorUri

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

WebFriendlyFeed.rar (35,28 kb)

Ado.Net Data Services 1.5 - Projections

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

Merhaba Arkadaşlar,

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

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

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

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

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

using System.Data.Services;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Projections.rar (53,71 kb)

Ado.Net Data Services Ders Notları - Security

Pazartesi, 2 Şubat 2009 06:16 by bsenyurt

Değerli Okurlarım Merhabalar,

Yazılım dünyasının en önemli zorluklarından biriside uygulamanın kapsamına göre güvenliğin etkili bir şekilde nasıl sağlanacağı ile ilişkilidir. Burada hassas bilgilerin korunması, kullanıcıların tanınması ve yetkilendirilmesi, kodun erişim ilkelerinin belirlenmesi, verinin şifrelenmesi gibi pek çok faktör söz konusudur. Genel anlamda günvelik farklı şekillerde göz önüne alınabilir.

  • Kimi zaman uygulama içerisinde kullanılan parametrik dış ortam değişkenlerini korumak gerekir. Örneğin uygulamanın kullandığı değişkenlerin konfigurasyon(app.config, web.config gibi) dosyasında şifrelenerek saklanması önemlidir ki bu pek çok uygulama standardınında ilkeleri arasında yer almaktadır.
  • Kimi zaman uygulamanın içerisindeki kodların ne tür işlemler yapabileceğinin belirlenmesi(Kode Erişim Güvenliği-Code Access Security) önemlidir. Örneğin uygulamanın, kurulduğu sistem üzerinde dosya yazma yetkisi olmamasının sağlanması, yada sadece dosya okuma yapmasına izin verilmesi veya uygulama içerisinden ağ ortamına bağlantıya izin verilmemesi gibi.
  • Kimi zaman uygulamayı açan kişilerin doğrulanması ve yapabileceklerinin sınırlandırılması gerekir(Authentication/Authorization). Örneğin uygulamayı açma yetkisi olan bir kullanıcının sahip olduğu role göre her menü seçeneğini kullanamaması gibi.

Vakalar ve gereklilikler çoğaltılabilir. Tek bir makine üzerinde kendi başına çalışan uygulamalar için güvenliğin sağlanması nispeten daha kolaydır. Ancak istemci/sunucu(Client/Server) bazlı mimariye geçildiğinde güvenliği sağlamak her zamankinden dahada zor bir hal almaktadır. Bunun en büyük nedenlerinden birisi farklı ortamlar arasında verinin, çeşitli protokollere göre mesajlar üzerinden transfer edilmesi gerekliliği ve bu nedenle iletişiminde güvenli hale getirilmesinin zorluğudur. Öyleki, mesajların şifrelenmesi, iletişim kanalının güvenli hale getirilmesi, aradaki mesajların yakalanması ve değiştirilmesi ihtimaline karşılık gerekli tedbirlerin alınması gibi kıstaslar söz konusudur. Yine durum istemci ve sunucu tarafındaki uygulamaların belirli olmaları halinde biraz daha kolay bir şekilde ele alınabilir. Oysaki sunucu tarafında bir servis uygulamasının bulunması ve buna herhangibir istemcinin bağlanabilecek olması gibi durumlarda ulusal bir takım güvenlik ilkelerine uygun olacak şekilde iletişimi sağlamak ve mesajlaşmak gerekmektedir.

Web servisleri göz önüne alındığında bu tip güvenlik konularını kolay bir şekilde tesis etmek adına WSE(Web Service Enhancements) alt yapısından yararlanılmaktadır. .Net Remoting tabanlı dağıtık çözümlerde sorumluluk neredeyse geliştiricinin kendisine aittir. Ancak Windows Communication Foundation uygulamaları göz önüne alındığında güvenlik, daha kolay ve etkili bir şekilde iletişim(Transport) veya mesaj(Message) seviyesinde ele alınabilmektedir ki bu kriterler şu anda konumuz dışındadır :)

Ado.Net Data Service' lerde temel olarak birer WCF servisidir. Bununla birlikte söz konusu servisler bilindiği üzere istemcilere RESTful modele göre hizmet vermektedir. Yani HTTP protokolünün GET,HEAD,DELETE,PUT gibi metodlarını ele alıp ATOM,XML,JSON gibi standartları kullanmaktadır. Basit bir servis olarak göz önüne alındığında güvenlik konusunda henüz yeteri kadar gelişmiş olmadığı düşünülebilir; mi acaba? İşte bu makalemizde daha çok bu soruya cevap bulmaya çalışacağız.

Her şeyden önce en önemli nokta Ado.Net Data Service' lerin veri kaynaklarını istemciye RESTful modeline göre sunmasıdır. Bu açıdan bakıldığında geliştiricilerin daha çok üzerinde duracağı nokta verinin erişilebilirliğinin güvenli hale getirilmesidir. Dolayısıyla Ado.Net Data Service' leri kullanan istemcilerin, servis tarafında bir şekilde doğrulanması(Authenticate) ve sonrasında durumlarına bakılarak yetkilendirilmeleri(Authorization) güvenliğin sağlanması adına önemlidir. Oysaki Ado.Net Data Service örnekleri, aslında herhangibir uygulama üzerinde host edilebilecek şekilde kullanılabilir. WCF kadar geniş bir konsepti yoktur. Bu nedenle kural basittir; doğrulama işlemlerinin sorumluluğu aslında Ado.Net Data Service örneğini host eden uygulamaya aittir. Bu anlamda, servisin bir WCF projesinde veya bir ASP.NET uygulamasında barındırılması doğrulama ve yetkilendirme işlemlerinin daha kolay ele alınabilmesi açısından önemlidir. Ado.Net Data Service' lerinde güvenlik 4 farklı alanda değerlendirilmektedir.

Kriter Güvenlik Alanı Açıklama
Host uygulamaya ait doğrulama modeli Authentication
(Doğrulama)
Servisi kullanacak olan istemcilerin doğrulanması için Host uygulama ortamı ele alınır. Söz gelimiz ASP.NET uygulaması üzerinde yapılan bir hosting işleminde built-in Membership API' si kullanılarak svc dosyalarına olan erişim kısıtlandırılabilir.
Servis Operasyonları
(Service Operations)
Authorization
(Yetkilendirme)
Metod bazlı operasyonlar yazılarak veriye olan erişim kısıtlandırılabilir. Örneğin HTTP Get metoduna göre sadece tek bir sonuçun elde edilmesine izin verilmesi(Single Result) sağlanabilir.
Veri Kesmeleri
(Data Interceptors)
İstemcinin talep ettiği verinin elde edilmesinden(Read) veya değiştirilmesinden(Update,Insert,Delete) önce işlemin kesilerek kısıtlamaların yapılması sağlanabilir. Örneğin kullanıcının yetkisine göre sadece görebileceği ürün listesinin verilmesi gibi.
Entity Görünürlüğü
(Entity Visibility)
Servisin dış ortama sunduğu Entity örneklerinin işlenme şekillerinin sınırlandırılmasıdır. Örneğin Product isimli Entity üzerinde sadece yazma işlemlerine izin verilmesi gibi.

Buradaki kriterlerin uygulanması ile bir Ado.Net Data Service ve içeriğine olan erişim yetkilendirilebilir. Aslında teknik detayları, geliştireceğimiz örneğin aralarına serpiştirerek devam edebiliriz. İlk olarak bize bir test servisi gerekmektedir. Konuyu kolay işlemek adına Ado.Net Entity Framework öğesini kullanaraktan Northwind veritabanındaki tüm tabloları ele aldığımızı düşünelim. Sonrasında ise basit bir Ado.Net Data Service öğesi geliştireceğiz. Peki ama bu öğeyi nerede barındıracağız? İşte burada kullanıcı doğrulamasını(Authentication) kolayca tesis edebileceğimiz bir Asp.Net uygulamasını göz önüne alabiliriz. Elbetteki Asp.Net Web Site Administration Tool' unu kullanaraktan bir kaç test kullanıcısı ve rolü oluşturmaktada yarar olacaktır. Örneklerimizde kullanacağımız kullanıcı bilgileri aşağıdaki gibidir.(Örnekte SQL Express Edition kullanılmış ve bu nedenle ASPNETDB.MDF dosyası web sitesinin olduğu App_Data klasörü altında oluşturulmuştur.)

Kullanıcı Şifre Rol
dealer1 dealer1. Dealer
dealer2 dealer2.
dealer3 dealer3.
region1 region1. Region
region2 region2.

Önemli unsurlardan biriside siteye olan ve amacımız gereği özellikle Ado.Net Data Service öğelerine olan erişimi kısıtlamaktır. Yani siteye isimsiz kullanıcı(Anonymous User) girişi kesin olarak engellenmelidir. Aşağıdaki ekran görüntüsüde, web.config dosyasında söz konusu engelleme için yapılmış olan değişikliler açık bir şekilde görülebilir.

Dikkat edileceği üzere Form tabanlı doğrulama kullanılmaktadır ve isimsiz kullanıcıların sisteme girmeleri yasaklanmıştır.

NOT : Elbetteki Form Tabanlı Doğrulama(Form Based Authentication) şart değildir. Özellikle Intranet sistemlerde Windows tabanlı doğrulama(Windows Based Authentication)' da ele alınabilir. Hatta istenirse Passport Tabanlı Doğrulama da etkinleştirilebilir. Hangisi kullanılırsa kullanılsın, servisi host eden web sisteminin doğrulama yetenekleri, Ado.Net Data Service çalışma zamanı tarafından ele alınabilmektedir.

Form tabanlı doğrulama söz konusu olduğundan varsayılan olarak aksi belirtilmedikçe(Asp.Net Güvenlik konularını hatırlayalım) Login.aspx isimli bir giriş sayfasına ihtiyacımız olacaktır. Bu sayfa üzerinde yine Asp.Net bileşenlerinden olan Login kontrolü kullanılabilir.

Sonuç olarak servis tarafındaki projenin durumu aşağıdaki şekilde olduğu gibidir.

Dikkat edileceği üzere servisi kullanacak olan kişilerin doğrulanması işlemini host uygulamanın kendisine vermiş bulunmaktayız. Buna göre kullanıcılar herhangibir şekilde servis dosyasını talep ettiklerinde, eğer bir bilete sahip değillerse, otomatik olarak Login.aspx sayfasına yönlendirileceklerdir. ASPNETDB.MDF veritabanı dosyasında yer alan ve etkin olan bir kullanıcı ile sisteme girildiğnde ise, kodlama tarafında karar vereceğimiz yetkilendirmeler devreye girecektir. Yani veriye olan erişim istenirse kullanıcıya göre kısıtlandırılabilecektir. Şimdi bu durumları analiz etmeye çalışalım. Konuyu son derece basit bir şekilde ele alacağımızdan NorthwindDataService.cs kod içeriğini aşağıdaki gibi yazmamız şimdilik yeterli olacaktır.

using System;
using System.Data.Services;
using System.Linq;
using System.Linq.Expressions;
using System.ServiceModel.Web;
using System.Web;
using NorthwindModel;
using System.Collections.Generic;

public class NorthwindDataService
    : DataService<NorthwindEntities>
{
    public static void InitializeService(IDataServiceConfiguration config)
    {
        // Rol tabanlı veri çekişi işlemleri örnek olarak gösterilebilir; HttpContext.Current.User.IsInRole();

        config.SetEntitySetAccessRule("*", EntitySetRights.All);
        config.SetEntitySetAccessRule("Employees", EntitySetRights.ReadMultiple);
        config.SetEntitySetAccessRule("Orders", EntitySetRights.WriteAppend);
        config.SetEntitySetAccessRule("Customers", EntitySetRights.None);

        config.SetServiceOperationAccessRule("CustomerCities", ServiceOperationRights.All);
        config.SetServiceOperationAccessRule("MySuppliers", ServiceOperationRights.All);

        // Eğer alttaki satır açılırsa FilterForProducts metodu devre dışı sayılır ve Products entity' sine hiç bir şekilde erişilemez.
        //config.SetEntitySetAccessRule("Products", EntitySetRights.None);

    }

    [QueryInterceptor("Products")]
    public Expression<Func<Products, bool>> FilterForProducts()
    {
        string name = HttpContext.Current.User.Identity.Name;
   
        if (name == "dealer1")
            return p => p.Suppliers.SupplierID == 1 || p.Suppliers.SupplierID == 2;
        else if (name == "dealer2")
            return p => p.Suppliers.SupplierID == 3;
        else if (name == "dealer3")
            return p => p.Suppliers.SupplierID == 1 || p.Suppliers.SupplierID == 2 || p.Suppliers.SupplierID == 7;
        else
            return p => p.Suppliers.SupplierID != null;
    }

    [ChangeInterceptor("Products")]
    public void ProductChange(Products p, UpdateOperations operation)
    {
        switch (operation)
        {
            case UpdateOperations.Add:
                break;
            case UpdateOperations.Change:
                if(!HttpContext.Current.User.IsInRole("Region"))
                    throw new DataServiceException(405,"UnitPrice için bu değişikliğe izin verilmedi");
                break;
            case UpdateOperations.Delete:
                break;
            case UpdateOperations.None:
                break;
            default:
                break;
        }
    }

    #region Service Operations
   
    [WebGet]
    public IQueryable<string> CustomerCities()
    {
        return (from c in this.CurrentDataSource.Customers
            select c.City).Distinct();
    }

    // filter, orderby gibi operatörler ve key bazlı erişim gibi sorgulara izin verilmez. Sadece entity bazlı erişim söz konusudur
    [WebGet]
    public IEnumerable<Suppliers> MySuppliers()
    {
        return from c in this.CurrentDataSource.Suppliers
                orderby c.CompanyName
                select c;
    }

    #endregion
}

İlk olarak uygulamamızı test etmeye çalıştığımızda Login.aspx sayfasına yönlendirildiğimizi göreceğiz. Bir başka deyişle herhangibir isimiz kullanıcı,  servise doğrudan erişilmek istendiğinde http://localhost:1000/SecuritySolutions/login.aspx?ReturnUrl=%2fSecuritySolutions%2fNorthwindDataService.svc adresine gönderilecektir ve dolayısıyla doğrulama sürecine girilmiş olacaktır. Eğer geçerli bir kullanıcı bilgisi ile giriş yapabilirsek bu durumda standart olarak servise erişebildiğimizi ve izin verilen sorgulamaları yapabildiğimizi göreceğiz. Şimdi kodu biraz analiz etmeye çalışalım. InitializeService metodu içerisine bakıldığında Entity seviyesinde bazı yetkilendirmeler yapıldığı görülmektedir.

config.SetEntitySetAccessRule("*", EntitySetRights.All);
config.SetEntitySetAccessRule("Employees", EntitySetRights.ReadMultiple);
config.SetEntitySetAccessRule("Orders", EntitySetRights.WriteAppend);
config.SetEntitySetAccessRule("Customers", EntitySetRights.None);

config.SetServiceOperationAccessRule("CustomerCities", ServiceOperationRights.All);
config.SetServiceOperationAccessRule("MySuppliers", ServiceOperationRights.All);

Buna göre tüm Entity kümelerine erişim hakkı izni, ilk satırdaki kod ile verilmiştir. Bu yetkilendirme, ilk satırdaki * ve EntitySetRights.All ile sağlanmaktadır. Ne varki ikinci satırda Employees Entity içeriği için, ReadMultiple kısıtlaması yapılmıştır. Buna göre Employees kümesi üzerinde örneğin anahtar bazlı sorgulara izin verilmeyecektir. Yani Employees(2) gibi bir talepte bulunulursa HTTP 403 Forbidden hatası alınır. Gerçektende durum Fiddler aracı yardımıyla izlendiğinde aşağıdaki ekran görüntüsünde yer alan sonuçlar ile karşılaşılır.

Dikkat edileceği üzere istemci taraına döndürülen XML içeriğinde Forbidden mesajı yazılmaktadır. Aynı zamanda hata kodu 403' tür. Bu durum istemci uygulama tarafından değerlendirilmelidir. Şu anda ilk yetkilendirmemizi Entity seviyesinde yapmış bulunuyoruz. Görüldüğü üzere, tüm Entity' lere erişim hakkı verilmiş olmasına rağmen Employees üzerinde bir kısıtlama uygulanmıştır. Üçüncü satırdaki kısıtlamaya göre Orders kümesi için sadece yeni öğe eklenmesine izin verilmektedir. Bu nedenle Orders entity içeriği yine tarayıcı üzerinden sorgulanmak istendiğinde HTTP 403 Forbidden hatası alınacaktır. Ancak istemci bir uygulama tarafından, Orders isimli veri kümesine yeni öğelerin eklenmesi işlemine izin verilecektir. Son olarak Customers Entity' sine herhangibir şekilde erişim izni kesinlikle verilmemektedir. Nitekim EntitySeyRights enum sabiti için None değeri verilmiştir.

Lakin burada CustomerCities isimli bir operasyon için izin verildiği gözlemlenmektedir. CustomerCities isimli operasyon string tabanlı IQueryable tipinden bir referans döndürmektedir. İçerideki sorgu cümlesine bakıldığında Customers Entity' si içerisindeki City adlarının Distinct fonksiyonu ile benzersiz olacak şekilde döndürüldüğü görülmektedir. Dolayısıyla Customers veri kümesini dış ortama tamamen kapatıp, kendisine ait şehir adlarını istemciye sunan bir operasyon tanımlaması söz konusudur ki buda bir güvenlik tedbiri olarak düşünülebilir. Yine devam eden satırda MySuppliers isimli bir servis operasyonuna tüm haklar ile erişim izni verilmiştir. Bu servis operasyonuna bakıldığında ise IEnumerable<Suppliers> tipinden bir referans döndürdüğü görülmektedir. Operasyon içerisindeki LINQ sorgusunda özel bir ifade yoktur. Ancak metodun dönüş tipinin IEnumerable olmasının bir anlamı vardır. Buna göre Suppliers tablosu sorgulanırken filter, orderby gibi operatörler kullanılamaz. Ayrıca anahtar bazlı erişimlere de (örneğin Suppliers(2) gibi) izin verilmez. Sadece sonuç kümesinin ham hali istemciye sunulur. Buda sonuç itibariyle bir kısıtlamadır. Nitekim çalışma zamaında örneğin, SupplierId değeri 3 olan Supplier bilgisini almak istediğimizde HTTP 400 Bad Request hatasını aldığımız görebiliriz.

Bu IQueryable ve IEnumerable arasındaki farkı biraz daha net bir şekilde görmüş bulunuyoruz. Yanlız dikkat edilmesi gereken bir nokta vardır. Servis operasyonunun döndürdüğü bir Entity içeriği var iken(örneğin IEnumerable<Suppliers>) InitializeService metodu içerisinde söz konusu tip için EnititySetRights.None değerinin kullanılması sonrasında servis çalışmayacaktır. Örneğimizde Customers için yapılan kısıtlamaya dikkat edildiğinde MyCustomers isimli operasyonun geriye string bazlı bir sonuç kümesi döndürdüğü görülmektedir. Bu servisin çalışmasına engel olmamaktadır.

Özellikle Entity tipleri ve servis operasyonları için yapılan erişim kısıtlamalarında devreye giren EntitySetRights enum sabitinin alabileceği değerler aşağıdaki tabloda belirtildiği gibidir.

EntitySetRights Değerleri
None Entity kümesine erişilmesi yasaklanmıştır. Metadata içerisinde görünmez ve üzerine okuma yazma işlemleri yapılamaz.
ReadSingle Entity üzerinde anahtar bazlı(Key Based) aramalara izin verilir. (Customers('ALFKI') gibi )
ReadMultiple Entity içeriğinin sorgulanmasına izin verilir, ancak anahtar bazlı erişimlere izin verilmez. Örneğin Employees(2) için sonuç kümesi elde edilemez.
WriteAppend Yeni Entity örnekleri eklenebilir.
WriteMerge Var olan Entity içeriği güncellenirken birleştirilme işlemi uygulanır.
WriteReplace Var olan Entity içeriği yenisi ile değiştirilerek güncellenir.
WriteDelete Silme işlemine izin verilir.
AllRead ReadSingle ve ReadMultiple değerlerinin birleşimidir.
AllWrite WriteInsert, WriteUpdate ve WriteDelete değerlerinin birleşimidir.
All Tam okuma ve yazma erişimine izin verilir.

Servis operasyonlarında erişim kısıtlamalarını belirleyen ServiceOperationRights enum sabitinin alabileceği değerler ise aşağıdaki tabloda görüldüğü gibidir.

ServiceOperationRights Değerleri
None Servis operasyonuna erişim izni yoktur.
ReadSingle Tek bir veri öğesinin okunmasına izin verilir.
ReadMultiple Servis operasyonu kullanılarak birden fazla veri öğesinin okunmasına izin verilir.
AllRead Tekil veya çoğul veri öğelerinin okunmasına izin verilir.
All Servis operasyonu için tüm haklar sağlanır.

Kodumuzda ilerlediğimizde, FilterForProducts ve ProductChange isimli iki metod ile karşılaşmaktayız. Bu metodlar veri kesme(Data Interceptor) fonksiyonellikleridir. Dikkat edileceği üzere FilterByProducts metodu içerisinde o anki kullanıcının adına bakılaraktan örnek bir içeriğin döndürülmesi sağlanmaktadır. Söz gelimi dealer2 isimli kullanıcı ile sisteme girildiğinde ve Products içeriği talep edildiğinde koddaki kesme metodunun içeriğine göre SupplierID değeri 3 olan listenin elde edildiği görülür. Özellikle durumu SQL Server Profiler aracı yardımıyla incelendiğinde gerçektende kesme metodunun devreye girdiği aşikardır.

Burada belkide en önemli nokta kesme işlemi sırasında, kullanıcı bilgisinin HttpContext.Current.User.Identity.Name ifadesi ile alınmasıdır. Bu tahmin edileceği üzere servisi kullanmak için Login olan kullanıcının adıdır.

Örneğimizde senaryoyu çok basit bir şekilde ele almak istediğimizden kullanıcı adlarının dealer1, dealer2, dealer3 olması halleri ele alınmıştır. Oysaki gerçek hayat senaryolarında daha faydalı bir kesme işlemi yapılabilir. Bununla ilişkili olaraktan sizlere bir alıştırma senaryosu örneği vermek isterim. Söz gelimi, kullanıcının hangi ürünlere bakacağı bilgisi, kullanıcının dahil olduğu bölgeye bağlı olabilir. Bu durumda ASPNETDB.MDF veritabanında yer alan kullanıcı bilgisi ile, kullanıcıların dahil olduğu bölgeleri tutan başka bir eşleştirme tablosu bu senaryo için çok yararlı olabilir. Böylece kesme metodu içerisinde, eşleştirme tablosundan yararlanılarak, giren kullanıcının sadece dahil olduğu bölgeye ait ürünleri görmesi sağlanabilir. Bunu kendi başınıza denemenizi ve yapmaya çalışmanızı öneririm.

NOT: Örneğimizdeki veri kesme metodları(Data Interceptors) içerisinde servisin host edildiği uygulama ortamının içeriğinin kullanıldığı görülmektedir. Burada Web ortamında olunmasının büyük bir avantaj sağladığı çok açıktır. Nitekim, o anki HTTP içeriğine HttpContext özelliği üzerinden ulaşılabilmektedir. Bu sebepten sisteme giriş yapmış olan kullanıcıyı tespit etmek son derece kolaydır. Ayrıca Ado.Net Data Service' lerin host edildiği web ortamlarında, Application, Session, Caching gibi yapılarında ele alınması mümkün hale gelmektedir.

Gelelim ProductChange metoduna. Bu metod içerisinde giriş yapan kullanıcın Region rolünde olması halinde değişiklik yapabilmesine müsade edilmektedir. Eğer giren kullanıcı Region rolünde değilse istemci tarafına HTTP Statu Code 405 mesajı gönderilmektedir. Aslında kesme operasyonlarını daha net bir şekilde ele alabilmek için bir istemci uygulama yazılmasında yarar vardır. Veri değiştirme işlemleri sırasında devreye giren bu metodda önemli olan noktalardan biriside UpdateOperations enum sabitinin kullanılmasıdır. Bu sabitin değerine göre kullanıcının nasıl bir operasyon gerçekleştirmek istediği kolayca tespit edilebilir. Metodun ilk parametresi kesme operasyonunun kime uygulanacağını işaret etmektedir. Buna göre söz konusu kesme operasyonları Products tipine uygulanabilir. Diğer önemli bir noktada istemci tarafına HTTP 405 mesajının DataServiceException tipinden bir nesne örneği fırlatılarak gönderiliyor olmasıdır. Çok doğal olarak bu istisna tipinin istemci uygulama tarafından ele alınıyor olması gerekmektedir. (Yani istemci tarafında try...catch...finally blokları kullanılarak istisna yönetimi yapılmalıdır.)

Söz konusu sistemde istemci uygulamanın, servisten talepte bulunurken belirli bir kullanıcı bilgisini göndermesi de şarttır. Aslında bu noktada Client Application Services' lerden faydalanılabilinir.(Bu konu ile ilişkili olarak daha önceki makalemi takip etmenizi öneririm) Özellikle .Net tabanlı istemcilerde İstemci Uygulama Servisinin kullanılmasını öneririm. Tabi bunun için servis tarafındaki konfigurasyon dosyasında bir takım değişikliklerin yapılması gerekmektedir. Bu sebeple host uygulamanın web.config dosyasında aşağıda yer alan eklemeleri yapabiliriz.

...
<appSettings/>

<system.web.extensions>
    <scripting>
        <webServices>
            <authenticationService enabled="true"/>
            <roleService enabled="true"/>
        </webServices>
    </scripting>
</system.web.extensions>

<connectionStrings>
...

İstemci uygulama tarafında ise proje özelliklerinden(Properties), Services kısmına geçmemiz ve geliştirdiğimiz web uygulamasının adresini işaret etmemiz yeterlidir.

Bölyece istemci uygulamanın doğrulama(authentication) ve rol(role) yönetimi için geliştirilen web uygulamasının üyelik sistemini(Membership API) kullanılacağı belirtilmiş olur. Örnek içerisinde Membership sınıfını kullanarak doğrulama yapacağımızdan System.Web.dll assembly' ının servis referansı ile birlikte istemci uygulamaya ekleniyor olmasıda gerekmektedir.

Servis tarafında isimsiz kullanıcıların(Anonymous Users) sisteme girişini kapattığımız için Add Service Reference kısmından servise ait WSDL içeriğini elde edemediğimizi görürüz. Nitekim Visual Studio ortamında söz konusu servis talep edildiğinde otomatikman web uygulamasının authentication kuralı devreye girmekte ve bizi Login.aspx sayfasına yönlendirmeye çalışmaktadır. Hal böyle oluncada servise ulaşılamamakta ve referansı elde edilememektedir.

Örnekte servisin host edildiği web uygulamasındaki deny user="?" kısmı, istemci uygulamayla aynı solution içerisindeki servis referansı eklendikten sonra etkin hale getirilmiştir. Bu elbetteki istenen çözüm değildir ve ayrıca daha sonrada servisin güncelleştirilmesi sırasında problemlere neden olmaktadır.

Diğer taraftan datasvcutil aracı yardımıylada istemci tarafı için gerekli tipler üretilmek istendiğinde benzer sonuçlar ile karşılaşılacaktır. Aşağıdaki ekran görüntüsünde ilk denemede anonymous kullanıcıların geri çevrildiği senaryo sonrası alınan uyarı mesajı görülmektedir. İkinci deneme yapılmadan önce ise web.config dosyasındaki deny users="?" kısmı allow users="?" olarak değiştirilmiş ve gerekli tiplerin üretildiği görülmüştür. Elbetteki buda istenen bir aktarım şekli değildir.

Bu noktada belkide servisin kullanılabilmesi için, istemci tarafınca gerek duyulan proxy tiplerinin önceden üretilip, kullanacak olan uygulamalara dağıtılması yöntemi tercih edilebilir. Açıkçası bu, servisi kullanacak olan istemcilerin belli olduğu durumlarda düşünülebilecek bir senaryodur ki pek çok büyük çaplı şirket içi projede göz önüne alınabilir.

Bu arada istemci tarafındaki Console uygulamasının içeriğini aşağıdaki gibi örnekleyebiliriz.

using System;
using System.Data.Services.Client;
using System.Linq;
using System.Net;
using ClientApplication.NorthwindServiceReference;
using System.Web.ClientServices;
using System.Threading;
using System.Web.Security;

namespace ClientApp
{
    class Program
    {
        static void Main(string[] args)
        {
            NorthwindEntities entities = new NorthwindEntities(
                new Uri("http://buraksenyurt:1000/SecuritySolutions/NorthwindDataService.svc")
                );

            entities.SendingRequest+=delegate(object sender,SendingRequestEventArgs e)
            {
                ClientFormsIdentity identity = Thread.CurrentPrincipal.Identity as ClientFormsIdentity;
                HttpWebRequest webRequest = e.Request as HttpWebRequest;
                if (identity != null)
                    webRequest.CookieContainer = identity.AuthenticationCookies;
            };

            try
            {
                if (Membership.ValidateUser("dealer1", "dealer1."))
                {
                    // QueryInterceptor için istemci kodu
                    var tumUrunler = from urun in entities.Products
                                                select urun;
   
                    foreach (var urun in tumUrunler)
                    {
                        Console.WriteLine(urun.ProductName);
                    }

                    // Http durum kodları(Http Status Code) için link-> http://en.wikipedia.org/wiki/List_of_HTTP_status_codes
       
                    // ChangeInterceptor için istemci kodu
                    var u = (from urun in entities.Products
                        where urun.ProductID == 1
                            select urun).First<Products>();
                    u.UnitPrice +=1;

                    entities.UpdateObject(u);
                    entities.SaveChanges();
                }
            }
            catch (DataServiceRequestException excp)
            {
                string excpMessage = String.Format("Status Code : {0}\n Inner Exception Message : {1} ",
                        ((DataServiceClientException)excp.InnerException).StatusCode.ToString(), excp.InnerException.Message
                    );
                Console.WriteLine(excpMessage);
            }           
        }
    }
}

Burada belkide en can alıcı nokta doğrulama için istemci tarafından kullanıcı ve şifre bilgilerinin nasıl gönderildiğidir. Dikkat edilecek olursa servise talep gönderilmeden önce ilgili doğrulama bilgileri yollanmaktadır. Membership sınıfının ValidateUser metodu yardımıyla kullanıcı doğrulandıktan sonra ise entity talebinde bulunulmakta ve bir güncelleştirme işlemi gerçekleştirilmektedir. Uygulamayı çalıştırdığımızda aşağıdaki ekran görüntüsü ile karşılaşırız.

Ürün bilgileri alınırken servis tarafındaki FilterForProducts isimli veri kesme metodu devreye girmiş ve delaer1 için SupplierID değerleri 1 veya 2 olanlar getirilmiştir. Yine dikkat edilecek olursa SaveChanges metodundan sonra servis tarafındaki ProductChange kesme metodunun devreye girmesi sonucu istemciye hata mesajı döndürülmüş ve bir istisna(exception) oluşmuştur. Bu son derece doğaldır nitekim dealer1 kullanıcısı Region rolünde değildir. Ancak örneğin region1 kullanıcısı ile giriş yaparsak bu durumda SupplierID değeri null olmayan ürünleri çekebildiğimizi ve aynı zamanda bunlardan ilkinin UnitPrice değerlerinide değiştirebildiğimizi görürüz. Hatta SQL Server Profiler aracı ile arka plandaki sorgu durumunu izlersek aşağıdakine benzer bir sorgunun işletildiğini kolayca izleyebiliriz.

exec sp_executesql N'update [dbo].[Products]
set [ProductName] = @0, [QuantityPerUnit] = @1, [UnitPrice] = @2, [UnitsInStock] = @3, [UnitsOnOrder] = @4, [ReorderLevel] = @5, [Discontinued] = @6
where ([ProductID] = @7)
',N'@0 nvarchar(4),@1 nvarchar(18),@2 decimal(19,4),@3 smallint,@4 smallint,@5 smallint,@6 bit,@7 int',@0=N'Chai',@1=N'10 boxes x 20 bags',@2=22.0000,@3=39,@4=0,@5=10,@6=0,@7=1

Örneklerdende görüldüğü üzere Ado.Net Data Service' lerde güvenliği sağlarken doğrulama(Authentication) ve yetkilendirme(Authorization) adına yapılabilecek belirli işlemler söz konusudur. Bu işlemler için bazı kuralların uygulanması gerekmektedir. Söz gelimi servis operasyonları göz önüne alındığında, yazılacak olan metodlarda dikkat edilmesi gereken kurallar şunlardır.

  • Metodun public erişim belirleyicisine sahip olması gerekmektedir.
  • Metodun dönüş tipi IQueryable<T> veya IEnumerable<T> olabilir. Buradaki T, Entity tipidir. Eğer operasyonun döndürdüğü sonuç kümesi üzerinde sıralama, sayfalama, filtreleme gibi işlemler yapılacaksa IQueryable<T> tipinin döndürülmesi gerekir.
  • HTTP Get metoduna uygun çağrılar için WebGet, HTTP Post,Delete,Put gibi talepler içinse WebInvoke niteliği(Attribute) kullanılmalıdır.

Benzer şekilde okuma işlemleri sırasındaki kesme fonksiyonelliklerininde uygulaması gereken bazı kurallar vardır. Buna göre;

  • Metodun public erişim belirleyicisine sahip olması gerekir.
  • Metoda [QueryInterceptor("EntityName")] niteliğinin uygulanması gerekir. EntityName yerine kesme işleminin uygulanacağı Entity tipinin adı verilir.
  • Metodun dönüş tipi Expression<Func<T,bool>> olmalıdır. Func temsilcisinde yer alan T entity tipidir.
  • Metod parametre almaz.
  • Metodun işleyişi sırasında bir istisna(Excpetion) oluşsa bile istemci talebi tamamlanır ve kendisine hata mesajı uygun HTTP Statu Code değeri ile döndürülür.

Eğer kesme operasyonu veri güncellenmesi,eklenmesi veya silinmesi işlemleri sırasında yapılacaksa, izlenilmesi gereken kurallar aşağıdaki gibidir.

  • Metodun public erişim belirleyicisi olmalıdır.
  • Metoda [ChangeInterceptor("EntityName")] niteliği uygulanmalıdır. Buradaki EntityName, entity tipinin adıdır.
  • Metodun dönüş tipi yoktur. Bu nedenle void olarak tanımlanır.
  • Metodun iki parametresi vardır. İlki entity tipi ikincisi ise UpdateOperations enum sabitidir. Bu enum sabiti ile metodu içerisinde değiştirme, silme, ekleme operasyonları ele alınabilir.
  • Metodun işleyişi sırasında bir istisna oluşursa, istemciden gelen talep tamamlanır ve kendisine uygun olan Http Statu Code değerine sahip hata gönderilir. Bu istisna sonrasında sunucu tarafındaki asıl veri kaynağında herhangibir değişiklik kesinlikle olmaz.

Makalemizi sonlandırmadan önce önemli bir noktayı daha vurgulamakta yarar vardır. Servisin doğrulanması için istemci tarafından gönderilen kullanıcı adı ve şifre bilgileri açık metinler olarak gitmektedir. Eğer örnekler test edilirken Fiddler aracı yardımıyla arka plandaki paketler izlenirse aşağıdaki ekran görüntüsünde yer alan durum ile karışalışır.

Bu nedenle en uygun çözüm servisin HTTPS tabanlı bir iletişim üzerinden hizmet vermesinin sağlanması olarak düşünülebilir.

Bu yazımızda Ado.Net Data Service' lerde doğrulama ve yetkilendirme işlemlerinin nasıl ele alınabileceğini, bir başka deyişle güvenliğin nasıl sağlanabileceğini en temel hatlarıyla incelmeye çalıştık. Ado.Net Data Service konusunda geliştirmeler devam etmektedir. Güvenlik ile ilişkili olaraktan farklı yaklaşımların getirilmeside bu nedenle söz konusu olabilir. Ancak en azından, host uygulamanın bu işte önemli bir rol üstlendiği gözden kaçırılmamalıdır. Bu yazıda kullanılan tekniğe göre, doğrulama işlemini Asp.Net Web uygulaması devralmıştır. Size tavsiyem bunu bir WCF sitesinden host ederkende gerçekleştiriyor olmanızdır. Konunun daha kolay pekişmesi adına aşağıdaki görsel dersleride izlemenizi tavsiye ederim. Böylece geldik bir makalemizin daha sonuna. Bir sonraki makalemizde görüşünceye dek hepinize mutlu günler dilerim.

Ado.Net Data Services-Security(1)

Ado.Net Data Services-Security(2)

Ado.Net Data Services-Security(3)

(Boyutun küçük olması için ASPNETDB.MDF ve log dosyası çıkartılmıştır. Bu nedenle örneği deneyebilmek için söz konusu veritabanını Asp.Net Web Site Administration Tool ile oluşturmanız gerekmektedir.)

Guvenlik.rar (312,54 kb)

Ado.Net Data Services Ders Notları - Optimistic Concurrency

Perşembe, 30 Ekim 2008 05:45 by bsenyurt

İstemci-Sunucu(Client-Server) bazlı uygulamalar göz önüne alındığında, istemcilerin aynı veriler üzerinde birbirlerinden habersiz şekilde değişiklikler yapabilme ihtimali oldukça meşhur bir vaka olarak bilinmektedir. Özellikle .Net tarafında bağlantısız katman(Disconnected Layer) uygulamalarında bu tip vakalar son derece önemlidir. Zaman zaman bu tip vakalar ile mücadele etmek ve tedbirler almak gerekir. Vaka aslında şu şekilde ifade edilebilir; "sunucu üzerinden aynı veri içeriklerini çeken istemci programlar, sunucu ile bağlantılarını kestikten sonra kendi uygulama alanları üzerine aldıkları verilerde değişiklik yapabilirler. Ancak bu noktada sunucu ile sürekli bir bağlantıları olmadığından, başka istemcilerin aynı veriler üzerinde değişiklikler yapıp yapmadıklarını tam olarak bilemezler. Bu sebepten aynı veriler üzerinde birbirlerinden habersiz olacak şekilde yaptıkları değişiklikleri sunucuya gönderebilirler." İşte bu noktada sunucu tarafında durumun nasıl ele alınacağı önem kazanır. Bu amaçla çeşitli denetleme mekanizmaları kullanılabilir. Bu yazımızda hepinizin kulağında bol bol Optimistic Concurrency kelimelerinin çınlayacağını şimdiden söyleyebilirim.

Bahsetmiş olduğumuz bu vaka bazen görmezden gelinebilecek olmasına karşın çoğu durumda kontrol altına alınması gereken bir sorun olarak değerlendirilir. İşin içerisine birde değişikliklerin sunucu üzerindeki veritabanına gönderilmesi sırasında devreye alınan Transaction' lar girerse, vaka kendi içerisinde dahada karmaşıklaşır. Ancak bizim şu anda istemediğimiz tek şey bu vakayı dahada karıştırmaktır. Bunlara karşın söz konusu vakada çözümsel olarak optimistic(iyimser) yada pesimistic(kötümser) yaklaşımların uygulanabilir olduklarınıda bilmek gerekir. Peki bu durumun Ado.Net Data Service' ler ile olan bağlantısı nedir? Herşeyden önce Ado.Net Data Service hangi modeli baz alır? Baz aldığı model nasıl uygulanır?

Bildiğiniz gibi bu ana kadarki ders notlarımızda ve görsel derslerimizde Ado.Net Data Service' lerin, web programlama modeline uygun olarak çalıştığını ve EDM(Entity Data Model) yada Custom LINQ Provider gibi katmanlar üzerinde veri sunumu gerçekleştirdiğine değindik. Ayrıca, Ado.Net Data Service' ler bir sunucu uygulama üzerinden host edilmek zorunda olmakla birlikte, bunları tüketen farklı istemci uygulamalar yazılabilmektedir. Bir başka deyişle tipik bir istemci-sunucu modeli söz konusudur. Bunlara ilaveten işin içerisinde, istemci tarafına çekilebilen veriler ve tabiki CRUD(CreateRetriveUpdateDelete) operasyonları söz konusudur. Bu operasyonlar içerisinde yer alan CUD fonksiyonellikleri ve istemcilerin veriyi kendi uygulama alanlarına çektikten sonra sunucu ile herhangibir bağlantılarının kalmayışı, istemcilerin aynı veriler üzerinde birbirlerinden habersiz değişikliker yapabilecekleri sonucunu doğurmaktadır. Peki bu tarz bir sorun ile nasıl mücadele edilebilir? Ado.Net Data Service çözüm olarak, Optimistic Concurrency yaklaşımının ele alınmasına izin vermektedir.

NOT : Optimistic Concurrency yaklaşımına göre, istemci tarafına çekilen veriler güncelleştirilmek üzere sunucu tarafına gönderildiklerinde ilk hali ile karşılaştırılırlar. Böylece ilk okumadan sonra başka bir istemcinin aynı veriyi değiştirip değiştirmediği kontrol altına alınabilir. Eğer bir değişiklik var ise istemcinin bu konuda uyarılması gerekir. Bu uyarıl üzerinde çalışılmakta olan modele göre çoğunlukla bir istisna(Exception) olarak ele alınır. Söz gelimi Ado.Net tarafından bildiğimiz DBConcurrencyViolation bu tip bir istisnadır. Tabi işin içerisine servis yönelimli bir çözüm girdiğinde bu, çoğunlukla bir Fault Message formatına uygun olacak şekilde hata bilgisi içeren bir XML verisidir. Eğer veriler başkası tarafından değiştirilmemişse tabiki güncelleme işlemi sunucu tarafındada onaylanacaktır.

Peki Ado.Net Data Service tarafında bu yaklaşım nasıl ele alınmaktadır? Artık bu noktadan sonra adım adım basit bir örnek üzerinde ilerlenilmesinde yarar olacağı kanısındayım. Örneğimizi geliştirirken en büyük yardımcılarımızdan biriside Fiddler isimli HTTP Debugging Proxy aracı olacaktır. Nitekim çakışma olması halinde istemciler ve sunucu arasında gidip gelen HTTP paketlerinin incelenmesi gerekmektedir. Test senaryomuz son derece basittir. Aynı veri satırı üzerinde değişiklik yapacak en az iki istemci uygulamanın çalıştırılması ve bu esnada oluşacak istisnaların(Exceptions) ve HTTP paketlerinin izlenmesi hedeflenmektedir. Bunların yanında SQL tarafında neler olduğunu gözlemlemek adınada SQL Server Profiler aracından yararlanmamız gerekecektir. Tabi öncelikli olarak basit bir WCF Service uygulaması geliştirerek başlamalıyız. Söz konusu servisimiz daha önceden geliştirdiğimiz Azon isimli veritabanını ve içerisinde bir kaç satır veri içeren Kitap isimli tabloyu istemci tarafına sunacak şekilde geliştirilecektir. Bu nedenle WCF Service uygulamımız için gerekli olan ön hazırlıklar aşağıdaki gibidir.

EDM(Entity Data Model) içeriğimiz;

Burada hemen bir özelliği vurgulamak gerekiyor. EDM diagramında yer alan Kitap Entity tipi içerisinde yer alan özelliklerin her birisi için Concurrency Mode isimli bir özellik yer almaktadır. Properties penceresinden ulaşılabilen bu özelliğin değeri varsayılan olarak None şeklindedir. Buna Mixed değerini vermemiz halinde Optimistic Concurrency için söz konusu özellik değerlerinin hesaba katılacağı belirtilmiş olunur. Örneğimizde bu amaçla Ad ve Fiyat özelliklerinin Concurrency Mode değerleri Mixed olarak belirlenmiştir. Ki senaryomuzda sadece bu alanların değerleri için Optimistic Concurreny kontrolü yapılacaktır.

Kitap tablosuna ait bir kaç satırlık veri içeriği;

AzonServices.svc.cs içeriğimiz;

using System;
using System.Data.Services;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel.Web;
using AzonModel;

public class AzonServices
    : DataService<AzonEntities>
{
    public static void InitializeService(IDataServiceConfiguration config)
    {
        config.SetEntitySetAccessRule("*", EntitySetRights.All);
    }
}

Tahmin edeceğiniz gibi istemci tarafında CRUD operasyonları yapılabileceğinden EntitySetRights.All enum sabiti değeri kullanılmıştır. Buraya kadar geldikten sonra AzonServices.svc isimli Ado.Net Data Service örneğinin çalıştığından emin olmakta yarar vardır. Bunun için servisi basit bir tarayıcı uygulama içerisinde açmamız yeterli olacaktır. Aşağıdakine benzer bir ekran görüntüsü ile karşılaşırız.

Yanlız burada Kitap entity içeriği talep edildiğinde, atom formatında üretilen XML verisinde yeni bir attribute tanımlaması karşımıza gelecektir.

Her bir entity elementi içerisinde m:etag isimli bir attribute(nitelik) tanımlanmıştır. m takma adlı isim alanına sahip bu nitelikler içerisinde her bir kitabın Ad ve Fiyat bilgilerinin yer aldığına dikkat edin. Hatırlayacağınız gibi, EDM diagramında bu özelliklerin Concurrency Mode değerlerini Mixed olarak belirlemiştik. Bu nedenle çakışma kontrolü için ilgili özelliklerin değerleri XML çıktısına dahil edilmiştir. Bundan dolayı etag yada entitytag adı verilen nitelikler içerisinde taşınan özellik değerleri, çakışma kontrolü için istemci ile sunucu arasında gidip gelen paketlerde önem kazanmaktadır.

Gelelim istemci uygulamamıza. Amacımız Optimistic Concurrency modelini Ado.Net Data Service' ler üzerinde incelemek olduğundan şimdilik işimizi görecek basit bir program yazmamız yeterli olacaktır. Sanıyorumki ne demek istediğimi anladınız :) Basit bir Console Application geliştiriyoruz. Uygulamamıza aynı solution içerisinde yer alan servisimizide ekledikten sonra istemci uygulama kodlarımızı aşağıdaki gibi geliştirebiliriz.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ClientApp.AzonServiceReference;

namespace ClientApp
{
    class Program
    {
        static void Main(string[] args)
        {
            // Proxy nesne örneği oluşturulur.
            AzonEntities proxy = new AzonEntities(new Uri("http://buraksenyurt:1000/AdventureHost/AzonServices.svc"));

            // Concurrency testi için ID si 81 olan Kitap verisi çekilir
            Kitap kitap81 = (from k in proxy.Kitap
                                        where k.KitapId==81
                                            select k).First<Kitap>();
   
            // 81 nolu ID' ye ait kitap bilgileri gösterilir
            Console.WriteLine("{0} : {1} : {2} : {3}",kitap81.KitapId,kitap81.Ad,kitap81.Fiyat,kitap81.StokMiktari);

            // Test amacıyla rastgele bir artış değeri üretilir ve 81 nolu Kitap nesne örneğinin Fiyat değeri değiştirilir
            Random rnd = new Random();
            int yeniFiyatArtisi = rnd.Next(1, 10);
            kitap81.Fiyat = kitap81.Fiyat + yeniFiyatArtisi;

            // Burası test noktası
            Console.WriteLine("{0} in fiyatı {1} olarak değiştirilecek. Onaylamak için tuşa basın.", kitap81.Ad, kitap81.Fiyat);
            Console.ReadLine();

            // Nesne güncellenir
            proxy.UpdateObject(kitap81);
   
            // Bir istisna bloğu içerisinde SaveChanges metodu çağırılır.
            try
            {
                proxy.SaveChanges();
                Console.WriteLine("İşlem tamam");
            }
            catch (Exception excp)
            {
                // Burada beklenen hata mesajı InnerException içerisinde gelir
                Console.WriteLine(excp.InnerException.Message);
            }

            Console.ReadLine();
        }
    }
}

Kısaca istemci uygulamada neler yaptığımızdan bahsedelim. Öncelikli olarak proxy nesnesi örnekleniyor. Fiddler aracını Web Development Server üzerinden çalıştıracağımız için daha önceki makalemizde bahsettiğimiz gibi 1000 numaralı portu ve makine adını kullanıyoruz. İlerleyen kısımlarda test amacıyla KitapID değeri 81 olan kitap bilgilerini istemci tarafına LINQ sorgusu üzerinden çekiyoruz. Elde edilen Kitap nesne örneğinin Fiyat özelliğini Random sınıfı ile üretilen rastgele bir değer kadar arttırıyoruz. Sonrasında ise bir Console.ReadLine çağrısı görmekteyiz. Bu çağrının olduğu yer aynı uygulamadan aynı makinede birden fazla çalıştırdığımızda test yapmamızı kolaylaştıracaktır. Bu çağrının ardından UpdateObject metodu işletiliyor. Sonrasında ise try...catch blokları içerisinde alınmış olan bir SaveChanges metodu çağrısı görüyoruz. Bu çağrı istemci tarafındaki güncelleştirmenin sunucuya iletilmesine neden oluyor. İşte bu noktada eğer bir çakışma söz konusu ise istemci tarafına bir exception döndürülecektir. Gelin hemen bir test yaparak işe başlayalım. Testimizde aynı uygulamadan iki adet çalıştırıyor ve birinde değişiklikleri sunucuya gönderdikten sonra , ikincisi içinde aynı işlemi yapmayı deniyoruz. Sonuç olarak bu test sonrasında aşağıdaki ekran görüntüleri oluşacaktır.

Her iki uygulama açılıp ilk uygulamadaki güncellemeler servis tarafında gönderildiğinde;

İkinci uygulamada devam edilip yeni değerler ile aynı veri güncellenmek üzere servise gönderildiğinde;

Görüldüğü gibi ikinci uygulamaya bir adet Fault Exception gönderilmiş ve etag değerinin Request Header' daki güncel etag değeri ile uyuşmadığı belirtilmiştir. Bir başka deyişle ikinci uygulamanın güncelleştirmek istediği satır başkası tarafından güncellenmiştir.

Hemen Fiddler aracı ile arka planda olanları inceleyelim. Birinci uygulama çalıştırıldığında ilk olarak 81 numaralı KitapID değerine sahip veri çekilmektedir. Bu tipik olarak HTTP Get çağrısıdır ve Request(İstek) ile Response(Cevap) paketlerine ait Header(Başlık) içerikleri aşağıdaki ekran görüntüsünde olduğu gibidir.

Standart bir iletişim olduğu gözlemlenmekle birlikte Respons Header içerisinde ETag isimli bir bilgi daha yer almaktadır. Bu bilgiye göre ilk uygulamaya çekilen kitap satırında Ad değeri Ado.Net Data Services Pro, Fiyat değeri ise 71.0000 dır. İkinci uygulamada aynı talepte bulunacaktır ve yine yukarıdaki ekran görüntüsünde yer alan paket alışverişi söz konusudur. Bu durumda ikinci uygulama için söz konusu olan Response Header içerisindeki ETag değeride aynıdır. Gelelim 3ncü paket alışverişine.

Request Header içerisinde If-Match isimli bir bilgi yer aldığı görülmektedir. Bu bilgiye göre Ado.Net Data Services Pro ve 71.0000 değerlerinin doğrulanması istenmektedir. Şu durumda başka bir uygulama yada veritabanı üzerinden doğrudan olacak şekilde, 81 numaları kayıtta bir değişiklik olmadığından Response Headers içerisinde söz konusu verinin güncellenen değerlerine ait bilgiler istemci tarafına gönderilmektedir. Bir başka deyişle şimdi ETag değerinin içeriği Ado.Net Data Services Pro ve 76.0000 dır. Dikkat edileceği üzere Fiyat değişmiştir. Ancak arkada unutmamamız gereken ikinci uygulamamız vardır. Bu uygulmada tuşa basıp devam edildiğinde, Fiddler üzerinden 4ncü paket alışverişi aşağıdaki gibi yakalanmaktadır.

Bu kez Request Header içerisindeki ETag değeri 71.0000 değeri için talepte bulunur. Ancak az önceki uygulamada ETag değerinde yer alan Fiyat 76.0000 olarak değişmiştir. Dolayısıyla If-Match karşılaştırması başarılı olmayacaktır. Bunun sonucu olarakta geriye HTTP/1.1 412 kodu (Precondition Failed) döner.

NOT : HTTP 1.1 durum kodları(Status Codes) ve açıklamaları için WC3 üzerinden yayınlanan adresinden bilgi alabilirsiniz.

Çok doğal olarak bu hata için istemci tarafına bir exception bilgisi gönderilmiştir. Bu arada SQL tarafında neler olduğunuda bilmekte yarar vardır. Aynı süreç SQL Server Profiler üzerinden incelendiğinde ilk uygulamanın güncelleştirme işleminden hemen önce 81 numaları KitapID için bir Select sorgusu çalıştırıldığı sonrasında ise aşağıdaki SQL ifadesinin devreye girdiği görülür.

exec sp_executesql N'update [dbo].[Kitap]
set [Ad] = @0, [Fiyat] = @1, [StokMiktari] = @2, [KategoriId] = @3
where ((([KitapId] = @4) and ([Ad] = @5)) and ([Fiyat] = @6))
',N'@0 nvarchar(25),@1 decimal(19,4),@2 int,@3 int,@4 int,@5 nvarchar(25),@6 decimal(19,4)',@0=N'Ado.Net Data Services Pro',@1=76.0000,@2=35,@3=1,@4=81,@5=N'Ado.Net Data Services Pro',@6=71.0000

Dikkat edelim! Where ifadesinden sonra KitapID, Ad ve Fiyat alanları hesaba katılmıştır. Bunun en büyük nedeni EDM diagramında Concurrency Mode değeri Fixed olarak belirlenen özelliklerdir. Dolayısıyla bu Where kriteri bozulmadığından güncelleştirme işlemi yapılmıştır. Oysaki ikinci uygulama tuşa basılarak devam ettirildiğinde, Update sorgusunun çalıştırılmadığı onun yerine 81 nolu KitapID için bir Select sorgusunun işlediği görülür. Akabindede zaten ETag verilerindeki uyuşmazlık nedeni ile istemciye bir hata mesajı gönderilmektedir. Buraya kadar anlattıklarımızın özetini aşağıdaki tablo ile özetleyebiliriz.

İşlem Açıklama (HTTP Trafiği ve SQL tarafı)

Birinci Uygulama Çalışır.

Kitap kitap81 = (from k in proxy.Kitap
                                        where k.KitapId==81
                                            select k).First<Kitap>();
HTTP Get Paketi Gönderilir.
Select sorgusu 81 no için çalışır.
Response bilgisi HTTP 200 Ok.
ETag bilgisi "Ado.Net Data Services Pro, 71.0000"
İkinci Uygulama Çalışır.
Kitap kitap81 = (from k in proxy.Kitap
                                        where k.KitapId==81
                                            select k).First<Kitap>();
HTTP Get Paketi Gönderilir.
Select sorgusu 81 no için çalışır.
Response bilgisi HTTP 200 Ok.
ETag bilgisi "Ado.Net Data Services Pro, 71.0000"

Birinci Uygulamada Fiyat bilgisi güncellenir.

Birinci Uygulamada SaveChanges metodu çalışır. HTTP Merge paketi gider.
If-Match bilgisi "Ado.Net Data Services Pro, 71.0000" dir. Karşılaştırma doğrudur.
SQL tarafında Update sorgusu çalışır güncelleme yapılır.
Response için ETag değeri  "Ado.Net Data Services Pro, 76.0000" olur.
İkinci Uygulamada Fiyat bilgisi güncellenir.
İkinci Uygulamada SaveChanges metodu çalışır. HTTP Merge paketi gider.
If-Match bilgisi "Ado.Net Data Services Pro, 71.0000" dir.
SQL tarafında 81 için veriler istenir.
If-Match bilgisindeki Fiyat verisi için uyuşmazlık vardır.
HTTP/1.1 412 (Precondition Failed) gönderilir.

Tabi işin bir de diğer şeklini ele almak gerekir. Yani Fixed değerlerini kullanmadığımız durum. Burada sadece servis tarafındaki EDM diagramında değişiklik yapmak yeterli olacaktır. Bir başka deyişle istemci tarafında, Concurrency modelinin değiştirildiğine dair bir servis güncellemesi yapılmasına gerek yoktur. Tabi böyle bir durumda her iki uygulamanın güncelleme işlemleride geçerli olacaktır ve buna görede en son yazanın verisi tabloya yansıtılacaktır. Aynı örneği buna göre test ettiğimizde SQL tarafına giden Update sorgularının aşağıdakine benzer olduğu görülmektedir.

exec sp_executesql N'update [dbo].[Kitap]
set [Ad] = @0, [Fiyat] = @1, [StokMiktari] = @2, [KategoriId] = @3
where ([KitapId] = @4)
',N'@0 nvarchar(25),@1 decimal(19,4),@2 int,@3 int,@4 int',@0=N'Ado.Net Data Services Pro',@1=88.0000,@2=35,@3=1,@4=81

Dikkat edileceği üzere sadece Primary Key alanı hesaba katılmıştır. Yine Fiddler aracı ile istemci ve servis arasındaki HTTP trafiği incelendiğinde If-Match yada ETag gibi bilgilerin Request veya Response Header' ları içerisinde yer almadığı görülür. Görüldüğü üzere senaryonun gerektirdiklerine göre servis tarafında Optimistic Concurrency modeli tercih edilebilir veya edilmez. Eğer bu model tercih edilirse istemci tarafındaki uygulamalarda mutlaka Exception kontrolünün yapılması gerekmektedir. Böylece geldik bir yazımızın daha sonuna. Bir sonraki yazımızda görüşünceye dek hepinize mutlu günler dilerim.

Concurrency.rar (39,02 kb)

Ado.Net Data Services Ders Notları - Custom LINQ Provider ve CUD Operasyonları

Cuma, 24 Ekim 2008 05:38 by bsenyurt

Ado.Net Data Services konusu ile ilintili bir önceki ders notlarımızda, EDM(Entity Data Model) üzerinden CUD(CreateUpdateDelete) işlemlerinin nasıl yapılabileceğini incelemeye çalışmıştık. Ancak durum özel LINQ Provider kullanımı söz konusu olduğunda biraz daha karmaşıklaşmakta. Nitekim Custom LINQ Provider kullanılması halinde istemci tarafından gelen CUD taleplerine karşılık servis tarafında özel kodlamaların yapılması gerekiyor. Bu noktada ders notlarımız içerisinde belkide çoğumuzun korkup fazla bulaşmak istemediği bir konuya kısacada olsa değineceğimizi şimdiden ifade etmek isterim. Reflection(Yansıma) :)

"Hayda brea nereden çıktı bu reflection" diyenelerimiz eminim ki vardır. Öyleyse kısaca bu kavramı hatırlamaya çalışalım. Reflection teknikleri ile çalışma zamanında(Runtime) .Net CLR tiplerine ait(ister kullanıcı tanımlı ister önceden tanımlanmış tipler) metadata bilgilerine ulaşılabilmektedir. Bu açıdan bakıldığında özellikle plug-in tabanlı uygulama geliştirmelerde, IDE tasarımlarında kullanılmaktadır. Hatta çalışma zamanında tiplere ait canlı nesne örneklerinin üretilip kullanılması bile mümkündür. Söz gelimi .Net Reflector gibi araçlar Reflection teknikleri yardımıyla geliştirilirler. Peki konunun Ado.Net Data Service' ler ile olan ilişkisi nedir? Neden bu tekniklere ihtiyaç vardır?

NOT : Reflection tekniklerinin kalbinde Type isimli tip yer alır. Bu basit tipten yararlanarak çalışma zamanında herhangibir tipe ait bilgileri elde etmek, tiplere ait nesne örnekleri oluşturmak gibi işlemler yapılabilir. Bu basit işlemler ile plug-in uygulamaları, Reflection gibi .Net Assembly' larının içeriğini gösteren programlar yazılabilir. Hatta IDE geliştirmelerindede Reflection tekniklerinden yararlanılmaktadır.

Bu sorunun sorulmasını nedeni servis tarafında Custom LINQ Provider kullanılmasıdır. İstemciler CUD işlemleri için servis tarafına nesne verisi içeren HTTP paketleri gönderirler. Servis tarafında yer alan herhangibir LINQ Provider' ın bu nesne içeriklerini ilgili veri kaynaklarına eklemesi, çıkartması yada değiştirmesi için çalışma zamanının anlayabileceği belirli kurallara uyması gerekir. Böylece herhangibir Custom LINQ Provider alt yapısının CUD işlemlerini gerçekleştirebilmesi bir standart altına alınmış olunur. Öyleyse burada servis tarafında uyulması gereken bir kurallar dizisi söz konusudur. Bu kurallar öyle bir yapı içerisinde tanımlanmalıdırki .Net CLR' a özgü olmalıdır. İşte bu noktada nasıl bir tip olabilir sorusunu kendinize sormanız gerekmektedir? Interface(Arayüz). Bilindiği üzere bir arayüz, uygulandığı tipin uyması gereken kuralları belirtmektedir. Ancak bunun yanında çok biçimlilik özelliğine sahiptir ve bu nedenle çalışma zamanında kendisini implemente eden tiplere ait nesne örneklerini taşıyabilmektedir. Buda plug-in tabanlı mimarilerde önem arz eden bir konudur. Bu şimdiki senaryomuzda gerekli bir bilgi değildir belki ama arayüzlerin ne işe yaradığınıda anlatan güzel bir tanımlamadır.

NOT : Interface(Arayüz) tipi sadece kendisini uyarlayan tiplerin uyması gereken üye(member) tanımlamalarını içerir. Bunun dışında iş yapan üyeler(members) içermez. Ayrıca polimorfik bir başka deyişle çok biçimliliğe destek verirler. Dolayısıyla bir arayüz tipini kullanarak çalışma zamanında kendisinden türeyen birden fazla nesneyi işaret etmek ve bunların hepsi için ortak işlemler yürütmek mümkündür. Nitekim bu ortak işlemler arayüz içerisinde tanımlanmış olup, arayüzü implemente eden tiplerde yazılma zorunluluğu bulunan ve farklı şekillerde uygulanabilen üyelerdir. (Bu noktada ah keşke şu C# derslerindeki temel konuları biraz daha araştırsaydım diyenleriniz olabilir. Vakit çok geç değil...)

Bu anlatılanlardan özet olarak şu sonucu çıkartabiliriz. Ado.Net Data Service içerisinde kullanılan taşıyıcı varlık tipinin(Container Entity Type) CUD işlemleri için belirli kurallara uyması ve bu kuralları uygulaması gerekmektedir. Bu dayatma için .Net CLR içerisinde System.Data.Services isim alanında yer alan IUpdatable adlı bir interface tipi tanımlanmıştır. Bu arayüzün üye metodları ile CUD işlemleri için gerekli uyarlamaların yapılması istenir. Bakın henüz Reflection tekniklerinden birisini kullanmaktan bahsetmediğimizi belirtelim. Bunu bir nesneyi servis tarafında örneklerken kullanıyor olacağız.

Şu anda neler hissetiğinizi biliyoru ve size hak veriyorum. Bu yazılanlar biraz sıkıcı. Aslında adım adım bir örneğe geçerek ilerlemekte yarar var. Gelin hiç vakit kaybetmeden bir WCF Service uygulaması geliştirelim ve içerisinde Custom LINQ Provider kullanan bir Ado.Net Data Service öğesi oluşturalım. Bu amaçla açılan WCF Service uygulamasına sırasıyla aşağıdaki tipleri entegre etmemiz yeterli olacaktır. İlk olarak servis tarafındaki geliştirmelerimizin genel görünümüne sınıf diagramı görüntüsünden bir bakalım.

Sınıf diagramı(Class Diagram);

Öncelikli olarak Entity tipimizi geliştirelim. Product isimli sınıfımız basit olarak bir ürüne ait Id, ad, fiyat, stok miktari gibi bilgileri taşıyacak şekilde tasarlanmıştır.

Product sınıfı;

using System;
using System.Linq;

namespace ServerApp
{
    public class Product
    {
        public int ProductID { get; set; }
        public string Name { get; set; }
        public double ListPrice { get; set; }
        public int UnitsInStcok { get; set; }
    }
}

Product entity tipini içerisinde kullanan ve istemcilere sunan taşıyıcı tipimiz ise aşağıdaki gibidir. Yanlız burada dikkat edilmesi gereken en önemli nokta söz konusu tipin IUpdatable arayüzünü uygulamış olmasıdır.

ShopEntites sınıfı;

using System;
using System.Linq;
using System.Reflection;
using System.Data.Services;
using System.Collections.Generic;

namespace ServerApp
{
    // CUD işlemlerine destek vermesi amacıyla ShopEntities tipine IUpdatable arayüzü uyarlanmıştır.
    public class ShopEntities
        :IUpdatable
    {
        // Product tipinden listeyi tutacak generic koleksiyonumuz
        static List<Product> _products;

        // Static yapıcı metod(Constructor) içerisinde bir kereliğine _product koleksiyonu örnek veriler ile doldurulur.
        static ShopEntities()
        {
            _products = new List<Product>
                {
                    new Product{ ProductID=1, Name="Dvd Player", ListPrice=100, UnitsInStcok=100},
                    new Product{ ProductID=2, Name="Mp3 Player 8 Gb", ListPrice=50, UnitsInStcok=150},
                    new Product{ ProductID=3, Name="15.4 inch LCD", ListPrice=190, UnitsInStcok=50},
                    new Product{ ProductID=4, Name="320 Gb Hdd", ListPrice=120, UnitsInStcok=200},
                    new Product{ ProductID=5, Name="1 Tb Hdd 3.5inch", ListPrice=250, UnitsInStcok=175}
                };
        }

        // İstemci tarafına sunulacak olan readonly(yanlız okunabilir) özellik
        public IQueryable<Product> Products
        {
            get
            {
                // AsQueryable<> ile generic List koleksiyonunun sorgulanabilir olması sağlanır
                return _products.AsQueryable<Product>();
            }
        }

        #region IUpdatable Members
   
        // Yeni bir Product nesnesinin oluşturulması ve eklenmesi sırasında devreye girer
        public object CreateResource(string containerName, string fullTypeName)
        {
            // Activator kullanılarak tipin full adından bir nesne oluşturulması sağlanır
            var newProduct = Activator.CreateInstance(Type.GetType(fullTypeName));
            // Oluşturulan nesne örneği Product tipine dönüştürülerek _products isimli generic koleksiyona eklenir
            _products.Add((Product)newProduct);
            // Eklenen yeni nesne örneği metoddan geriy döndürülür
            return newProduct;
        }

        // Silme işlemi sırasında devreye giren metod
        // Parametre olarak silinecek entity nesne örneği gelir
        public void DeleteResource(object targetResource)
        {
            // Gelen nesne örneği Product tipine dönüştürülür ve _products isimli koleksiyondan Remove metodu ile çıkartılır
            _products.Remove((Product)targetResource);
        }

        // Update(güncelleme) işlemi sırasında devreye giren metoddur.
        public object GetResource(IQueryable query, string fullTypeName)
        {
            object r = null;
            var numarator = query.GetEnumerator();
            while (numarator.MoveNext())
            {
                if (numarator.Current != null)
                {
                    r = numarator.Current;
                    break;

                }
            }
            return r;
        }

        // gelen nesnenin belirtilen özelliğinin değerini geriye döndüren metoddur
        public object GetValue(object targetResource, string propertyName)
        {
            var targetType = targetResource.GetType();
            PropertyInfo targetProperty = targetType.GetProperty(propertyName);
            return targetProperty.GetValue(targetType, null);
        }

        // Gelen nesnenin ilgili özelliğine gelen değeri atayan metoddur.
        // Bu metod veri ekleme ve güncelleştirme işlemleri sırasında ilgili nesne örneğinin her bir özelliği için çalışır
        public void SetValue(object targetResource, string propertyName, object propertyValue)
        {
            Type targetType = targetResource.GetType();
            PropertyInfo targetProperty = targetType.GetProperty(propertyName);
            targetProperty.SetValue(targetResource, propertyValue, null);
        }

        public object ResolveResource(object resource)
        {
            return resource;
        }

        // Değişikliklerin kaydedilmesini sağlayan metoddur.
        // Söz konusu örnekte veriler bellek üzerinden tutulduğundan uygulanmasına gerek yoktur.
        public void SaveChanges()
        {
        }

        public void RemoveReferenceFromCollection(object targetResource, string propertyName, object resourceToBeRemoved)
        {
            throw new NotImplementedException();
        }

        public object ResetResource(object resource)
        {
            throw new NotImplementedException();
        }

        public void SetReference(object targetResource, string propertyName, object propertyValue)
        {
            throw new NotImplementedException();
        }

        public void AddReferenceToCollection(object targetResource, string propertyName, object resourceToBeAdded)
        {
            throw new NotImplementedException();
        }

        public void ClearChanges()
        {
            throw new NotImplementedException();
        }

        #endregion
    }
}

Woooww! Evet biraz korkutucu bir implemantasyon. Ama en azından Custom Membership Provider yazarkenki kadar korkutucu değil :) Aslında tüm arayüzün uyarlamasını gerçekleştirmiş değiliz. Nitekim söz konusu örnekte kullanılan Product tipi içerisinde herhangibir navigasyon özelliği bulunmamaktadır. Diğer taraftan sadece CUD işlemleri için gerekli temel metodların uygulandığını ifade edebiliriz. Herşeyden önce CreateResource metoduna dikkat etmemiz gerekiyor. Notlarımızın başında belirtmiş olduğumuz gibi Reflection tekniklerinin burada iyi bir kullanımını görüyoruz. Nitekim ilk olarak parametre olarak gelen string ifadeden yararlanılarak bir nesne örneği oluşturuluyor ki bu noktada .Net Remoting günlerinden hatırlayacağımız meşhur Activator sınıfı kullanılmakta. Yine GetValue ve SetValue metodları içerisinde parametre olarak gelen bilgilerden yararlanılarak bir özellik değerinin set edilmesi veya elde edilmesi işlemlerinde Reflection teknikleri kullanılıyor. Tabi bizim odaklanmak istediğimiz nokta Ado.Net Data Service içerisinde Custom LINQ Provider kullanılması halinde CUD işlemlerinin nasıl gerçekleşeceği. Buraya kadar yaptıklarımız ile işin zor kısmını az da olsa aştık. Şimdi aşağıdaki svc içeriğine sahip Ado.Net Data Service öğesinide WCF projemize ekleyelim.

using System;
using System.Data.Services;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel.Web;
using ServerApp;

public class ShopServices
     : DataService<ShopEntities>
{
    public static void InitializeService(IDataServiceConfiguration config)
    {
        config.SetEntitySetAccessRule("*", EntitySetRights.All);
    }
}

CUD işlemeri gerçekleştireceğimiz için SetEntitySetAccessRule metodu içerisinde All sabit değerini kullanıyoruz. Artık istemci tarafını ve istemci için gerekli test kodlarını yazabiliriz. Her zamanki gibi basit bir Console uygulaması işimizi görecektir. Elbette servisimizide Add Service Reference seçeneği ile istemci uygulamaya eklememiz gerekiyor. İstemci tarafında yapılan servis ekleme işlemi sonrasında aşağıdaki sınıf diagramında görülen tipler oluşacaktır.

Daha önceki ders notlarımızdan da hatırlayabileceğimiz gibi, veri ekleme işlemleri için AddToProducts metodu oluşturulmuştur. Bunun dışında DataServiceContext üzerinden UpdateObject, DeleteObject metodlarınıda kullanıyor olacağız. Nitekim bu metodlar ile, silme ve güncelleme operasyonları gerçekleştirilecektir. İstemci uygulamamızın kod içeriği ise örnek olarak aşağıdaki gibi geliştirilebilir.

İstemci uygulama kodları;

using System;
using System.Linq;
using System.Collections.Generic;
using ClientApp.ShopServiceReference;

// Not: Kod içerisinde Exception kontrolleri yapılmamıştır
namespace ClientApp
{
    class Program
    {
        static void Main(string[] args)
        {
            ShopEntities proxy = new ShopEntities(new Uri("http://buraksenyurt:1000/ServerApp/ShopServices.svc"));

            // Yeni bir Product nesne örneği oluşturulur
            Product newProduct = new Product
                {
                    ProductID = 6,
                    Name = "HP Mouse",
                    ListPrice = 12,
                    UnitsInStcok = 100
                };

            // Nesne örneği eklenir
            proxy.AddToProducts(newProduct);

            // Ekleme operasyonu servis tarafına gönderilir
            proxy.SaveChanges();

            // ProductID değeri 2 olan Product nesnesi istenir.
            var prd = (from p in proxy.Products
                            where p.ProductID==2
                            select p).First<Product>();

            // Bazı özelliklerin değerleri sembolik olarak değiştirilir
            prd.ListPrice += 10;
            prd.UnitsInStcok += 15;
            // Nesne güncellemesi yapılır
            proxy.UpdateObject(prd);

            // Update operasyonu servis tarafına gönderilir
            proxy.SaveChanges();

            // Silme işlemine örnek olması için son eklenen Product istenir
            var lastPrd = (from p in proxy.Products
                                where p.ProductID == newProduct.ProductID
                                select p).First<Product>();
       
            // Nesne silinmesi yapılır
            proxy.DeleteObject(lastPrd);
            // Delete operasyonu servis tarafına gönderilir
            proxy.SaveChanges();       
        }
    }
}

Örnekte ilk olarak bir Product nesne örneği oluşturulmakta ve sonra AddToProducts metodu ile ilave edilmektedir. Tabiki bu ekleme işleminin servis tarafına gönderilmesi için, SaveChanges metodunun çağırılması gerekir. Güncelleme işlemi için UpdateObject metodu kullanılmaktadır. Silme işlemi içinse DeleteObject metodu. Elbette her iki işleminde HTTP paketleri içerisine gömülerek servis tarafına gönderilmesi için SaveChanges metodu çağırılmalıdır. İstemci uygulama bu haliyle çalıştırıldığında herhangibir sorun olmadan işlemlerin gerçekleştiği görülür. (Ancak siz bu uygulamayı test ederken mutlaka Debug modda çalıştırın ve küçük bir tavsiye; WCF tarafındaki debug işlemleri için hem servis hemde istemci uygulamayı debug modda çalıştırmalısınız.)

Uygulama test edilirken eğer SaveChanges metodlarında breakpoint' ler ile ilerlenir ve arka planda Fiddler gibi bir Http Debugging Proxy aracı kullanılırsa uygulamanın tamamının çalışması sonrasında aşağıdaki sonuçların elde edildiği görülür.

NOT : Fiddler aracı basit bir HTTP Debugging Proxy uygulamasıdır ve IIS üzerinde 80 numaralı porta gelen ve giden tüm HTTP paketlerini izleyebilmenizi, içeriğini görebilmenizi sağlar. Lakin Asp.Net Development Server ile kullanımında bazı ön ayarlar yapılması gerekmektedir. Fiddler aracının kullanımı Ado.Net Data Services' lerde Batch Processing işlemlerinin ele alındığı görsel dersimizde incelenmiştir.

HTTP Paketlerinin Fiddler aracı üzerinden incelenmesi;

Bu durumu biraz analiz edelim. İlk olarak yeni bir Product ekleniyor. Bu ekleme işlemi istemci tarafında yapılan SaveChanges metodu çağrısı sonrasında, servis tarafına bir HTTP paketi olarak gidiyor ki bu POST metoduna göre hazırlanmış bir pakettir. Yine Fiddler yardımıyla paketin içeriğinin aşağıdaki gibi olduğu görülebilir.

Header içeriği;

Paket içeriği;

Sonrasında ise bir HTTP Get talebi gelmektedir. Nitekim kod içerisinde güncelleme örneği için, ProductID değeri 2 olan ürün bilgisi istenmiştir. Bunun sonucu olarak tabiki istemci tarafına da bir içerik gönderilmektedir. Yine Fiddler aracı yardımıyla bu içeriğe bakılabilir.

ProductId değeri 2 olan ürünün istemci tarafına çekilmesinin ardından bir güncelleme işlemi gerçekleştirilir. Koda dikkat edecek olursak bu işlemler için UpdateObject ve ardından SaveChanges metodları çağırılmaktadır. SaveChanges metodu bu kez istemciden servis tarafına HTTP Merge paketi gönderir ve bu paket içerisinde güncelleştirilen yeni değerler yer alır. Buna göre servise gelen Request paketinin Header içeriği aşağıdaki gibidir.

Paket ile gönderilen bilgiler ise yine Fiddler aracı ile görülebilir.

Son olarak silme operasyonunda önce silinmek istenen veri servis tarafından talep edilmiştir. Bu noktada yine bir HTTP Get çağrısı gerçekleşir. Bu işlemden sonra istemci kodlarında DeleteObject ve ardından yine SaveChanges metodları çağırılmıştır. SaveChanges çağrısı sonrasında ise bu kez bir HTTP Delete talebi istemciden servis tarafına doğru gönderilecektir.

Arka planda HTTP paketlerinde neler gittiğini gördük. Burada Fiddler aracına çoğunuzun aşık olduğunu hisseder gibiyim. Aynı SQL Server Profiler gibi son derece başarılı bir izleme aracı.

Gelelim kod tarafındaki metod işleyişlerine. Söz gelimi veri ekleme işlemi sırasında istemci tarafında AddToProducts ve SaveChanges metodlarını kullanıyoruz. Peki ya servis tarafında uyguladığımız IUpdatable arayüzüne ait hangi metodlar devreye giriyor. Bu durumu analiz etmek için Debug modda biraz dolaşmam gerektiğini ifade etmek isterim. Sonunda aşağıdaki sonuçlara ulaşabildim. Tabi söz konusu süreçler yukarıdaki geliştirdiğimiz örneğe göre işlemektedir.

Insert işlemine ait süreç;

İstemci tarafında ilk olarak AddTo[EntityName](örneğin AddToProducts) metodu çağırılır. Sonrasında ise SaveChanges metodu yürütülür. SaveChanges metodu insert işlemi için gerekli HTTP paketinin Post metoduna göre servis tarafına gönderilmesinde rol oynar. Bunun karşılığında servis tarafında sırasıyla CreateResource, SetValue, SaveChanges ve ResolveResource metodları çağırılır. CreateResource metodu ile HTTP paketinde gelen istekte yer alan tipin, çalışma zamanında oluşturulması ve ilgili veri kaynağına eklenmesi işlemleri gerçekleştirilir. SetValue metodu ise oluşturulan tipin her özelliği için çalışır. Bir başka deyişle özelliklerin değerlerinin verilmesinde devreye girer. Örnekte veriler bellekteki koleksiyonlarda tutulduğu için geri dönüş değeri olmayan ve parametre almayan SaveChanges metodu içerisinde herhangibir işlem yapılmamıştır.

Update işlemine ait süreç;

İstemci tarafında bu kez öncelikli olarak UpdateObject ve sonrasında SaveChanges metodları çağırılır. Servis tarafında ise öncelikli olarak güncellenecek verinin elde edilmesi için GetResource metodu devreye girer. Insert sürecine benzer bir şekilde SetValue metodu güncellenecek özellikler için tek tek çalışır ve yine sırasıyla SaveChanges, ResolveResource metodları devreye girer.

Delete işlemine ait süreç;

Silme işleminde istemci tarafında sırasıyla DeleteObject ve SaveChanges metodları çalışır. Bunun karşılığında Delete metodunu içeren bir HTTP paketi servis tarafına gönderilir. Servis tarafında ise yine silinmek istenen verinin elde edilmesi için GetResource metodu ilk olarak devreye girer. Sonrasında ise DeleteResource ile bu verinin kaynaktan çıkartılması sağlanır. Örnekte bir koleksiyon kullanıldığından bu basit bir Remove işleminden öte değildir. Son olarak yine SaveChanges ve ResolveResource metodları çağırılır.

Görüldüğü üzere kendi geliştirdiğimiz Custom LINQ Provider' ların kullanıldığı senaryolarda en kritik nokta IUpdatable arayüzünün kullanılmasıdır. Bu arayüzün yazımızda kullanılan örnek implemantasyonu ile basit CUD işlemleri gerçekeştirilebilir. Böylece geldik Ado.Net Data Service' ler ile ilişkili bir ders notumuzun daha sonuna. Bir sonraki yazımızda görüşünceye dek hepinize mutlu günler dilerim.

UsingIUpdatable.rar (39,03 kb)

Ado.Net Data Services Ders Notları - CUD Operasyonları

Perşembe, 16 Ekim 2008 05:33 by bsenyurt

Ders notlarımızı tutmaya devam ediyoruz. Bu gün Ado.Net Data Service' ler yardımıyla istemcilerden veri ekleme(Insert), silme(Delete) ve güncelleme(Update) işlemlerinin nasıl yapılabileceğini incelemeye karar verdim. Tabiki Ado.Net Data Services konusu halen daha Astoria kod adıyla anılmakta. Dolayısıyla zaman içerisinde uygulanan metod adlarında ve kullanılış biçimlerinde değişiklikler olması muhtemel. Yine şu an itibariyle neler yapabileceğimize bakmakta yarar var nitekim bir WCF fanatiği olarak Ado.Net Data Services açılımı beni son derece heyecanlandırıyor. Bu kadar laf salatasından sonra kısaca konuya girmeye ve basit bir örnek geliştirmeye ne dersiniz?

Ado.Net Data Service operasyonlarına yapılan istemci çağrılarının HTTP bazlı olduklarını ve GET,POST,PUT,DELETE gibi metodlara göre uygulandıklarını biliyoruz. İstemci tarafından servis operasyonlarına doğru eklenmek, silinmek veya güncellenmek amacıyla gönderilen verilerin çoğunluğuda POST metoduna uygun olacak şekilde paketlenmektedir. Ancak elbetteki istemci tarafında bu paketin manuel olarak hazırlanması gibi işlemler ile uğraşmamıza gerek yoktur. Nitekim istemci tarafında oluşturulan servis örneğine ait üye metodlar yardımıyla bu paketlerin otomatik olarak hazırlanması, gönderilmesi sağlanabilmektedir. Her zamanki gibi adım adım ilerleyeceğimiz bir örnek konuyu pekiştirmek açısından çok daha yararlı olacaktır.

İlk olarak veritabanı üzerindeki hazırlıklarımızı yapalım. Örneğimizde Azon isimli(Benim seminerlerimi takip edenler bu isimdeki hayali şirketi hatırlayacaktır :) ) bir veritabanını ve bunun üzerinde yer alan Kategori ve Kitap isimli tabloları kullanıyor olacağız. Şimdi hiç vakit kaybetmeden aşağıdaki SQL Script' ini SQL Management Studio üzerinde çalıştırabilir ve örnek veritabanı, tablo ve test verilerinin eklenmesini sağlayabilirsiniz.

--Test veritabanı oluşturulur
Create Database Azon
GO

--Test veritabanını kullan
Use Azon
GO

-- Kategori tablosu oluşturulur
Create Table Kategori
(
    KategoriId int identity(1,1) not null,
    Ad nvarchar(20) not null,
    Constraint Pk_Kategori Primary Key(KategoriId)
)
GO

--Kategori tablosu için test verileri girilir
Insert into Kategori (Ad) Values ('Programlama');
Insert into Kategori (Ad) Values ('SOA Çözümleri');
Insert into Kategori (Ad) Values ('Web Programlama');

--Kitap tablosu oluşturulur
Create Table Kitap
(
    KitapId int Identity(1,1) not null,
    Ad nvarchar(50) not null,
    Fiyat money not null,
    StokMiktari int not null,
    KategoriId int not null,
    Constraint Pk_Kitap Primary Key(KitapId)
)
GO

--Kitap tablosu için test verileri eklenir
Insert into Kitap (Ad,Fiyat,StokMiktari,KategoriId) Values ('Her Yönüyle C#',50,100,1);
Insert into Kitap (Ad,Fiyat,StokMiktari,KategoriId) Values ('Essential C# 3.0',80,25,1);
Insert into Kitap (Ad,Fiyat,StokMiktari,KategoriId) Values ('Programming WCF',75,120,2);
Insert into Kitap (Ad,Fiyat,StokMiktari,KategoriId) Values ('SOA For Dummies',35,45,2);
Insert into Kitap (Ad,Fiyat,StokMiktari,KategoriId) Values ('Asp.Net 3.5 Step By Step',70,80,3);

--Relation Oluşturulur
ALTER TABLE Kitap WITH CHECK ADD CONSTRAINT FK_Kitap_Kategori FOREIGN KEY(KategoriId)
REFERENCES Kategori (KategoriId)
GO

ALTER TABLE Kitap CHECK CONSTRAINT FK_Kitap_Kategori
GO

Kategori ve Kitap tabloları yine bir birlerine bağlı kümeler olarak tasarlanmıştır. Böylece bir Kategori ve buna bağlı Kitap satırlarının Entity örnekleri üzerinden eklenmesinin analizi için gerekli ortam hazırlanmış olur. Her iki tablo arasındaki ilişkinin veritabanı üzerinde tanımlanmış olmasıda son derece önemlidir. Nitekim bu tanımla yapılmadığı takdirde EDM(Entity Data Model) içerisinde oluşturulan Entity tipleri arasındada bir Association en azından otomatik olarak oluşmayacaktır. Sıradaki aşamada servisimiz için gerekli host uygulamanın yazılması gerekmektedir. Ado.Net Data Service' leri bu ana kadarki ders notlarımızda sürekli olarak WCF Service şablonları üzerinde tuttuk. Şimdilik geleneği bozmuyoruz. Yine işlerimizi kolaylaştırması açısından EDM katmanını kullanıyor olacağız. Daha önceki ders notlarımızda ve görsel derslerimizde bu konuya değindiğimiz için tekrar etmeyeceğiz ancak oluşan EDM modelinin aşağıdakine benzer olması gerektiğinide hemen vurgulayalım.(Bu aşamayı tamamlarken önceki ders notları veya görsel derslere bakmadan ilerlemeye çalışmanız sizin yararınıza olacaktır. Bildiğiniz gibi pratik mükemmelleştirir.)

KitapServisi olarak isimlendirdiğimiz svc dosyasına ait kod içeriği ise aşağıdaki gibi olmalıdır.

using System;
using System.Linq;
using System.Data.Services;
using System.ServiceModel.Web;
using System.Collections.Generic;
using AzonModel;

public class KitapServisi
    : DataService<AzonEntities>
{
    public static void InitializeService(IDataServiceConfiguration config)
    {
        config.SetEntitySetAccessRule("*", EntitySetRights.All);
    }
}

Tahmin edileceği üzere AzonEntites isimli taşıyıcı tip içerisindeki tüm Entity tipleri tüm haklar ile istemcilere açılmaktadır. Burada varsayılan olarak bulunan AllRead değerini kullanamayız. Nitekim verilerin eklenmesi, güncellenmesi ve silinmesi operasyonları için izin verilmesi gerekir. Burada kolaya kaçarak All enum sabiti değerini kullandık. Bu adıma kadar geldikten sonra yapmamız gereken ilk iş servisin çalışıp çalışmadığını test etmek olmalıdır. Bu amaçla servisin herhangibir tarayıcıda açılması yeterlidir. Eğer bir sorun yoksa aşağıdaki ekran görüntüsüne benzer bir çıktının elde edilmesi gerekir.

Sıradaki adımımızda istemci uygulamanın oluşturulması ve servis referansının eklenmesi yer almaktadır. Yine çok sevineceğiniz ve yazarken büyük keyif alacağınız bir Console Application :) geliştiriyor olacağız. Console uygulamamıza aynı solution içerisinde oluşturduğumuz Ado.Net Data Service örneğini ise Add Service Reference seçeneği ile ekleyeceğiz. Aynen aşağıdaki ekran görüntüsünde olduğu gibi.

Bu işlemin ardından solution içeriğinin aşağıdakine benzer olmasını bekleyebiliriz.

Artık birazda kod yazalım. İlk kod örneğinde toplu bir insert işleminide gerçekleştiriyor olacağız. İlk önce bir Kategori örneğini oluşturup istemci tarafındaki Entity nesnesine ilave edecek ve bu değişikliği veritabanına doğru göndereceğiz. Sonrasında ise bu Kategori altında olacak Kitap nesnelerini örneklerinin değerlerini veri tabanına göndereceğiz. İşte kodlarımız;

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ClientApp.AzonSpace;

namespace ClientApp
{
    class Program
    {
        static void Main(string[] args)
        {
            // Proxy nesnesi örneklenir
            AzonEntities proxy = new AzonEntities(new Uri("http://localhost:1630/HostApp/KitapServisi.svc"));

            // Yeni bir kategori nesnesi örneklenir
            Kategori windowsClient = new Kategori { Ad = "Windows Programlama" };

            // Oluşturulan Kategori nesne örneği bellek üzerindeki Entity örneğine ilave edilir
            proxy.AddToKategori(windowsClient);
            // Yeni kategorinin veritabanındaki tabloya eklenmesi için SaveChanges metodu çağırılır.
            // (Bu noktada SaveChanges çağırılması şart değildir. Bu sadece KategoriId' nin tablodan elde edilmesini sağlamada rol oynamaktadır)
            proxy.SaveChanges();
            Console.WriteLine("{0} ID si ile {1} Kategorisi eklendi",windowsClient.KategoriId.ToString(),windowsClient.Ad);

            // Generic bir List koleksiyonunda tutulacak şekilde Kitap nesne örnekleri oluşturulur.
            List<Kitap> kitaplar = new List<Kitap>()
                {
                    new Kitap { Ad = "Windows Form 2.0 Programming", Fiyat = 90, StokMiktari = 10 },
                    new Kitap { Ad = "Pro WPF", Fiyat = 75, StokMiktari = 12 },
                    new Kitap { Ad = "Core Windows Programming", Fiyat = 90, StokMiktari = 16 }
                };

            // Tüm kitaplar dolaşılır
            foreach (Kitap k in kitaplar)
            {
                // Her bir Kitap nesne örneği ilgili Entity örneğine ilave edilir.
                proxy.AddToKitap(k);
                // O andaki kitap ile yukarıda oluşturulan Kategori arasındaki ilişki kurulur
                proxy.AddLink(windowsClient, "Kitap", k);
            }

            // Değişiklikler veritabanına gönderilir
            // Batch enum sabit değeri ile tüm isteklerin tek bir HTTP paketinde gönderilmesi sağlanır
            proxy.SaveChanges(System.Data.Services.Client.SaveChangesOptions.Batch);

            // Eklenen kategori servis tarafından talep edilir
            var eklenenKategori = (from k in proxy.Kategori
                                            where k.KategoriId == windowsClient.KategoriId
                                            select k).First();
            // Elde edilen kategoriye ait kitap bilgilerinin yüklenmesi istenir
            proxy.LoadProperty(eklenenKategori, "Kitap");
   
            Console.WriteLine("\n{0} kategorisine eklenen kitaplar\n",eklenenKategori.Ad);
            // Eklenen kategoriye bağlı kitaplar listelenir
            foreach (Kitap k in eklenenKategori.Kitap)
            {
                Console.WriteLine("{0} {1} {2} {3}",k.KitapId.ToString(),k.Ad,k.StokMiktari.ToString(),k.Fiyat.ToString("C2"));
            }
        }
    }
}

Kodlarda özellikle üzerinde durmamız gereken nokta bir Entity nesne örneğinin oluşturulması sonrasında kullanılan AddTo[EntityAdı], AddLink ve SaveChanges isimli metodlardır. AddTo[EntityAdı] metodları, servis referansının eklenmesi sırasında istemci tarafında oluşturulan Entity tiplerinin her birisi için service tipine eklenir. Söz gelimi Kitap için AddToKitap, Kategori içinse AddToKategori. Bunu sınıf diagramındanda görebiliriz.

AddTo[EntityAdı] metodları parametre olarak aldıkları Entity nesne örneklerini taşıyıcı servis tipinin takip etmesinde rol oynarlar. Aslında kendi içlerinde AdoToObject metodunu çağırmaktadırlar. Bu izleme işlemi aslında SaveChanges metodu için önem arz etmektedir. Nitekim eklenen, silinen veya güncellenen nesne değerlerinin veritabanına doğru yansıtılması işlemini gerçekleştirmektedir. Bu noktada durup kodu debug ederek ilerlemenizi öneririm. Özellikle SaveChanges metodu çağırılmadan önce proxy nesne örneği içerisinde Kategori veya Kitap özellikleri içeriği ile SaveChanges çağrısı sonrası içeriklere bakıldığında durum daha net bir şekilde görülebilir. Söz gelimi ben testleri yaparken aşağıdaki sonuçları elde ettim.

SaveChanges çağrısı öncesi;

SaveChanges çağrısı sonrası;

SaveChanges metodunun çağırılması sırasında birde System.Data.Services.Client.SaveChangesOptions.Batch enum sabiti değeri kullanılmıştır. Bu değer ile birden fazla HTTP paketinin yerine tüm isteğin tek bir HTTP paketi içerisinde gönderilmesi sağlanabilmektedir bu servis ile istemci arasındaki trafik akışının yoğunluğunu azaltıcı bir etkendir. (Bu konu ile ilişkili olarak(Batching) bir görsel ders hazırlıyor olacağım.)

Tabi SaveChanges çağrısı sırasında sunucu tarafındaki veri kaynağı üzerindede bir takım SQL sorgu ifadeleri çalışacaktır. Burada SQL Server Profiler aracının kullanmanızı şiddetle tavsiye ederim. Örneğin veri ekleme testleri sırasında benim yakaladığım örnek sql ifadeleri aşağıdaki gibi olumuştur.

-- Kategori için Insert çağrısı
exec sp_executesql N'insert [dbo].[Kategori]([Ad])
values (@0)
select [KategoriId]
from [dbo].[Kategori]
where @@ROWCOUNT > 0 and [KategoriId] = scope_identity()',N'@0 nvarchar(19)',@0=N'Windows Programlama'

-- İlk Kitap için Insert çağrısı
exec sp_executesql N'insert [dbo].[Kitap]([Ad], [Fiyat], [StokMiktari], [KategoriId])
values (@0, @1, @2, @3)
select [KitapId]
from [dbo].[Kitap]
where @@ROWCOUNT > 0 and [KitapId] = scope_identity()',N'@0 nvarchar(28),@1 decimal(19,4),@2 int,@3 int',@0=N'Windows Form 2.0 Programming',@1=90.0000,@2=10,@3=27

-- İkinci kitap için Insert çağrısı
exec sp_executesql N'insert [dbo].[Kitap]([Ad], [Fiyat], [StokMiktari], [KategoriId])
values (@0, @1, @2, @3)
select [KitapId]
from [dbo].[Kitap]
where @@ROWCOUNT > 0 and [KitapId] = scope_identity()',N'@0 nvarchar(7),@1 decimal(19,4),@2 int,@3 int',@0=N'Pro WPF',@1=75.0000,@2=12,@3=27

-- Üçüncü kitap için Insert çağrısı
exec sp_executesql N'insert [dbo].[Kitap]([Ad], [Fiyat], [StokMiktari], [KategoriId])
values (@0, @1, @2, @3)
select [KitapId]
from [dbo].[Kitap]
where @@ROWCOUNT > 0 and [KitapId] = scope_identity()',N'@0 nvarchar(24),@1 decimal(19,4),@2 int,@3 int',@0=N'Core Windows Programming',@1=90.0000,@2=16,@3=27

İlk kodda dikkat çekici noktalardan biriside Kategori nesne örneği eklendikten sonra Kitap nesne örneklerinden oluşan bir koleksiyonun nasıl ilave edildiğidir. Burada döngü içerisinde çağırılan AddToKitap metodu haricinde AddLink isimli bir fonksiyon yer almaktadır. Bu metod, o anki Kitap nesne örneğinin hangi Kategori' ye bağlı olacağının belirlenmesinde rol oynamaktadır. Bu sebepten dolayıda kodun SQL tarafındaki üretiminde 3 Kitap için çalıştırılan Insert sorgularında KategoriID değerleri otomatik olarak set edilmiştir. AddLink metodu çağırılmadığı takdirde bu ilişkinin sağlanması mümkün olmamaktadır. İlk örnek kodumuzu tamamlamış bulunuyor. İşte testler sırasında oluşan örnek bir ekran çıktısı.

Sırada güncelleştirme işlemleri var. Bu amaçla aşağıdaki örnek kod satırlarını göz önüne alabiliriz;

// Güncellenecek veri kümesi çekilir.
// Örneğin KategoriId değeri 1 olan Kitaplar çekilir
var tumKitaplar = from k in proxy.Kitap
                            where k.Kategori.KategoriId==1
                            select k;

// Elde edilen sonuç kümesindeki her bir Kitap nesne örneği üzerinde basit bir güncelleştirme yapılır
foreach (Kitap k in tumKitaplar)
{
    Console.WriteLine("Güncelleştirme öncesi {0} için Fiyat {1}",k.Ad,k.Fiyat.ToString("C2"));
    k.Fiyat += 10;
    // Yapılan güncellemeler entity üzerinde onaylanır
    proxy.UpdateObject(k);
}
// Değişiklikler veritabanına gönderilir
proxy.SaveChanges();

// Sonuçları test etmek için servis tarafından 1 numaralı kategoriye bağlı kitaplar tekrar istenir
Console.WriteLine("\nDeğişiklikler Sonrası Liste\n");
var kategori1Kitaplari = from k in proxy.Kitap
                                        where k.Kategori.KategoriId == 1
                                            select k;
// Her bir kitabın bilgisi ekrana yazdırılır
foreach (Kitap k in tumKitaplar)
{
    Console.WriteLine("Güncelleştirme öncesi {0} için Fiyat {1}", k.Ad, k.Fiyat.ToString("C2"));
}

Bu kod parçasında örnek olarak KategoriId değeri 1 olan Kategoriye bağlı Kitap nesnelerinin fiyatlarının 10 birim arttırılması sağlanmaktadır. Bizim için bu kod parçasında dikkat edilmesi gereken fonksiyonellikler UpdateObject ve yine SaveChanges metodlarıdır. SaveChanges metodu SQL tarafında aşağıdaki sorgu ifadelerinin oluşmasına neden olur.

-- İki Update yakalanır. Nitekim 1 numaralı kategoride sadece iki Kitap vardır.
exec sp_executesql N'update [dbo].[Kitap]
set [Ad] = @0, [Fiyat] = @1, [StokMiktari] = @2
where ([KitapId] = @3)
',N'@0 nvarchar(14),@1 decimal(19,4),@2 int,@3 int',@0=N'Her Yönüyle C#',@1=90.0000,@2=100,@3=1

exec sp_executesql N'update [dbo].[Kitap]
set [Ad] = @0, [Fiyat] = @1, [StokMiktari] = @2
where ([KitapId] = @3)
',N'@0 nvarchar(16),@1 decimal(19,4),@2 int,@3 int',@0=N'Essential C# 3.0',@1=120.0000,@2=25,@3=2

Burada iki adet Update sorgusunun oluşturulması son derece doğaldır. Nitekim 1 numaralı Kategori' ye bağlı sadece iki adet Kitap bulunmaktadır. Kodun çalıştırılması sonrasında ise programın ekran görüntüsü aşağıdakine benzer olacaktır.

Burada hemen bir test yapmanızı öneririm. İlk Console.WriteLine çağrısını UpdateObject metodunun sonrasına koyduğunuz takdirde Kitap nesne örneklerinin değerlerinin anında güncellenip güncellenmediğini(Entity tarafında) analiz edebilirsiniz.

Son olarak basit bir silme operasyonu işlemini ele alıyor olacağız. Bu son kod parçasındaki amacımız bir Kategori ve buna bağlı Kitap verilerinin silinmesini sağlamak. İşte örnek kod parçamız;

// Önce kullanıcıya Kategori listesi sunulur
Console.WriteLine("\nSilme Operasyonu\n");
var kategoriler = from k in proxy.Kategori
                        select k;
foreach (Kategori kategori in kategoriler)
{
    Console.WriteLine("{0} {1}",kategori.KategoriId.ToString(),kategori.Ad);
}
// Kullanıcıdan silmek istediği kategorinin KategoriId değeri istenir
Console.WriteLine("Silmek istediğini kategori id' yi seçin");
int secilenKategoriId;

// Eğer ekrandan alınan değer Int32' ye Parse edilebilirse
if (Int32.TryParse(Console.ReadLine(),out secilenKategoriId))
{
    Kategori secilenKategori = null;
    try
    {
        // Ekrandan girilen ID değerine ait Kategori nesne örneği talep edilir
        secilenKategori = (from k in proxy.Kategori
                                        where k.KategoriId == secilenKategoriId
                                            select k).First<Kategori>();

        // Önce bu Kategorinin KategoriId değerine sahip Kitap listesi alınır
        var kitapListesi = from k in proxy.Kitap
                                        where k.Kategori.KategoriId == secilenKategoriId
                                            select k;
        // Elde edilen her bir Kitap nesne örneği DeleteObject metodu ile çıkartılır
        foreach (Kitap kitap in kitapListesi)
        {
            proxy.DeleteObject(kitap);
            Console.WriteLine("{0} çıkartılacak",kitap.Ad);
        }

        // Son olarak seçilmiş olan Kategori nesnesi çıkartılır
        proxy.DeleteObject(secilenKategori);
        Console.WriteLine("{0} kategorisi çıkartılacak", secilenKategori.Ad);
   
        // Değişikliklerin veri kaynağı üzerinde de yapılması için SaveChanges metodu çağırılır.
        proxy.SaveChanges(System.Data.Services.Client.SaveChangesOptions.Batch);
        Console.WriteLine("Değişiklikler gönderildi...");
    }
    catch
    {
    }
}

Kodda öncelikli olarak kullanıcıya var olan Kategori listesi gösterilir ve silmek istediği Kategoriye ait KategoriId değerini girmesi istenir. Bunun sonrasında söz konusu Kategori ve buna bağlı Kitaplar bulunur. Önce Kitap nesne örnekleri tek tek DeleteObject metodu ile çıkartılmak üzere işaretlenir. Sonrasında ise aynı işlem seçilen Kategori için yapılır. Son olarak tüm işlemlerin SaveChanges metodu ile veritabanına gönderilmesi sağlanır. Bu noktada SQL tarafında oluşturulan sorgu ifadeleri aşağıdakilere benzer olacaktır. (Bu ifadelerin yakalanması için SQL Server Profiler aracını kullandığımızı hatırlayalım)

-- 43 nolu KategoriId değerine sahip Kitap verileri silinir
exec sp_executesql N'delete [dbo].[Kitap]
where (([KitapId] = @0) and ([KategoriId] = @1))',N'@0 int,@1 int',@0=75,@1=43

exec sp_executesql N'delete [dbo].[Kitap]
where (([KitapId] = @0) and ([KategoriId] = @1))',N'@0 int,@1 int',@0=76,@1=43

exec sp_executesql N'delete [dbo].[Kitap]
where (([KitapId] = @0) and ([KategoriId] = @1))',N'@0 int,@1 int',@0=77,@1=43

-- 43 nolu KategoriId değerine sahip Kategori silinir
exec sp_executesql N'delete [dbo].[Kategori]
where ([KategoriId] = @0)',N'@0 int',@0=43

Sonuç olarak örnek ekran çıktısı aşağıdaki gibi olacaktır.

Burada hemen bir noktayı vurgulayalım. Normal şartlar altında aynı silme operasyonunu veritabanı üzerinde gerçekleştirmek istesek, önce Kitap verilerini sonra ise Kategori verilerini silmemiz gerekirdi. Bildiğinin gibi bunun nedeni Kategori ve Kitap tabloları arasında tanımlı olan Foreign Key bağımlılığı ve bunun sonucu olan Constraint tir.Aynı mantık kod tarafında şart değildir. Bir başka deyişle önce Kategori için DeleteObject sonrasında ise Kitap topluluğu için DeleteObject metodu çağırılabilir. Nitekim organizasyon ve doğru SQL çalıştırma sırası SaveChanges metodu sonrasında oluşmaktadır. Bunu kod üzerinde deneyerek test etmenizi öneririm.

Buraya kadar yaptıklarımızı kısaca özetlersek eğer veri ekleme, güncelleme ve silme işlemleri sırasında SaveChanges metodunun asıl işi yüklendiğini ve veri kaynağında sorgu ifadelerinin oluşturulması için gerekli HTTP paketlerini hazırladığını düşünebiliriz. İstemci tarafında nesne örneği eklemek için AddTo[EntityAdı] yada AddToObject metodlarını kullanabileceğimizi gördük(Sonuçta AddTo[EntityAdı] kendi içinde AddToObject metodunu çağırmakta). Ayrıca silme işlemlerinde DeleteObject ve güncelleştirme işlemlerinde ise UpdateObject fonksiyonlarını ele aldık. Diğer taraftan ilişkisel nesnelerin bağlanması içinse AddLink fonksiyonelliğinin ele alınması gerektiğini öğrendik. Nihayetinde bir ders notumuzun sonunda daha geldik. Ancak akıllarda(en azından benim aklımda ve biraz sonra sizin aklınızda) şöyle bir soru oluşabilir. Eğer servis tarafında özel bir LINQ Provider ve buna bağlı tipler kullanılıyorsa Insert,Update ve Delete işlemleri nasıl gerçekleştirilebilir? Nitekim EDM modelinde veri kaynağında SQL olduğundan bu işlemler için SQL sorgu ifadeleri kullanılmaktadır. İşte bu analizi bir sonraki ders notlarımızda inceliyor olacağız. Böylece geldik bir makalemizin daha sonuna. Makalemizde yer alan CRUD işlemlerini şu ve bu görsel derslerden de inceleyebilirsiniz. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

InsertUpdateDelete.rar (47,82 kb)

Ado.Net Data Services Ders Notları - İstemci Geliştirmek

Pazartesi, 6 Ekim 2008 05:25 by bsenyurt

Hatırlayacağınız gibi daha önceki iki ders notumuzda Ado.Net Data Service örneklerinin nasıl geliştirilebileceğini incelemeye çalışmıştık. Hatırlatmak gerekirse, Ado.Net Data Service' ler ile verilerin Entity Data Model(EDM) veya Custom LINQ Provider bazlı katmanlar üzerinden REST modeline göre sunulması mümkün olmaktadır. Bu noktada söz konusu servislerin WCF' in REST modelini kullanan ve Ado.Net üzerine odaklanmış bir açılımı olduğu görüşünde hem fikir olabiliriz. Ne varki Servis Yönelimli Mimari(Service Oriented Architecture-SOA) temelli çözümlerde yap-bozun en önemli iki parçasını servis ve istemciler oluşturmaktadır. Bir başka deyişle, servislerin tamamlayıcısı olan ve ilgili hizmetleri kullanacak istemci uygulamalar(Client Applications) olmalıdır. İşte bu yazımızda istemci uygulamaları göz önüne alacağız.

Ado.Net Data Service ve istemci arasında geçen bu hikayede, anahtar öneme sahip bir kaç kelimede yer almaktadır. WCF, Ado.Net, REST vb. Bunlar az çok istemcilerin kimler olabileceğinide ortaya çıkartan terimlerdir. Aslında bir servis istemcisinin herhangibir uygulama olabilmesi istenir. Platform kriterleri gözetilmeksizin. Fakat geçmiş zamanlarda sadece belirli platformlara yönelik çözümler de ele alınmamış değildir ki halen daha popüler olarak pek çok alt yapıda kullanılmaktadır. Buna verilebilecek en güzel örnek belkide .Net Remoting çözümleridir. .Net Remoting temelli uygulamalar sadece .Net tabanlı istemci ve sunucuları baz almaktadır. Bu bir kısıtlamadır ama performans ve verimlilik gibi avantajlarıda getirmektedir. Ancak zaman ilerledikçe farklı tipte platformların ortaklaşa haberleşebilmesi daha büyük önem arz etmeye başlamıştır. Buda Xml Web Service' lerin popüler olmasının nedenlerinden birisidir :) Ama uzun zamandır elimizde çok daha güçlü bir kozun olduğunu da belirtmek isterim; Windows Communication Foundation.

Tekrardan sihirli kelimelerimize dönelim. WCF kelimesi, geliştireceğimiz servisin WCF kurallarının bir sonucu olarak ortaya çıktığının açık bir göstergesidir. Buda kendi içerisinde WCF' in nimetlerini barındıracak bir servis çözümünü ifade eder ki buna JSON(JavaScript Object Notation), Syndication, Web Programming Model gibi pek çok önemli kriterde dahil olur. Dolayısıyla WCF servislerini ele alabilen tüm istemci çeşitleri bu senaryoda olasıdır. Ancak Ado.Net kelimesi, servisimizi belirli bir yöne doğru odaklamaktadır. Buna göre geliştirilen servis tamamen verilerin(Data) sunumu üzerine konuşlandırılmaktadır. Bu zorunluluk olmamakla birlikte bütünlüğü sağlayıcı bir hedef olarak görülmelidir. Buda istemci tipini belirleyici diğer bir etkendir. Ancak REST(REpresentationalStateTransfer) kelimesi olayı biraz daha belirginleştirmektedir. Söz konusu istemciler REST modeline göre talepte bulunabilmelidir. Yani HTTP protokolü üzerinde GET,HEAD,POST,DELETE gibi metodlara göre talepte bulunabilmeli ve gelen sonuçlarıda irdeleyebilmelidirler.

Doğruyu söylemek gerekirse bu kelimeleri bir kenara bırakıp herhangi çeşit istemci uygulama ele alınabilir diyerekten işin içerisinden de sıyrılabiliriz :) Yinede güncel teknolojiler göz önüne alındığında aşağıdaki maddelerde yer alan istemci tiplerinin dikkat çekeceği ortadadır.

  • Ajax Tabanlı Web Sayfaları (Ajax Based Web Pages)
  • Silverlight Nesneleri
  • WPF(Windows Presentation Foundation), WinForms gibi Windows Uygulamaları
  • Diğer Servisler (WCF Servisleri, Xml Web Servisleri, .Net Remoting Uygulamaları, Windows Servisleri vb...)
  • Sınıf Kütüphaneleri-Class Libraries

Bu tipler çoğaltılabilir. Ancak benimde dikkatimi çeken ve özellikle üzerinde durulmaya değer çeşitler Ajax tabanlı web sayfaları ve Silverlight nesneleridir ki bunlar şu zaman itibariyle son derece popüler uygulamalardır. Yanlız dikkat edilmesi gereken başka noktalarda vardır. Söz gelimi bir Ado.Net Data Service örneği farklı servisler tarafındanda tüketilebilir. Böyle bir durumda tüketici servisin kendisi, aslında tükettiği servis için bir istemci olmaktadır. Yine extreme senaryolar göz önüne alınabilir. Söz gelimi Active Directory hizmetini özel bir LINQ Provider ile güvenli bir şekilde farklı bir lokasyona bir Ado.Net Data Service olarak sunabiliriz. Örneğin dünya üzerindeki bir otele ait tüm nesnel verilerin Active Directory kökenli olaraktan tek bir merkezde tutulduğunu düşünün. Diğer lokasyonlardaki oteller bu merkezi verileri kullanmak isteyecektir. Bu noktada tüketici istemciler bir servis olup söz konusu hizmeti kapalı ağ içerisinde(Intranet diyebiliriz) ele alabilir ve diğer istemcilere sunabilir. Ki bu kapalı ağ istemcileride söz konusu lokasyondaki otelin içerisinde yer alan çeşitli tipteki uygulamalardır. Bu tabiki gerçek bir vaka değil ancak sizlerde bu cümlede bir kaç dakikalığına durup çeşitli Ado.Net Data Service senaryoları düşünebilir ve bunları yakın çevrenizdeki yetkin kişiler ile tartışarak analiz edebilirsiniz.

İstemci hangi çeşitten olursa olsun servis ile olan iletişimini kod seviyesinde kolaylaştırmak açısından genellikle Proxy nesneleri göz önüne alınır. Bu bir zorunluluk değildir. Nitekim REST modeline göre servise gidecek olan HTTP paketlerinin manuel olarak hazırlanıp gönderilmeside mümkündür. Öyleki bu işlem için HttpWebRequest yada HttpWebResponse tipleri kolayca göz önüne alınabilir. Bir anlamda örneğin, XML Web Servislerinde bir SOAP(Simple Object Access Protocol) paketinin bahsettiğimiz tipler ile hazırlanıp gönderilmesinden ve geri gelen cevabın açılarak ele alınmasından farklı bir işlem değildir. Ne varki Proxy kullanımı kodlamacının işini oldukça kolaylaştırır. Çünkü bu sayede geliştirici bildik kodları yazarken sanki kendi ortamındaki bir nesneyi kullanıyormuş hissine kapılır. Gelip giden paket içerikleri ile uğraşmak zorunda kalmaz. Halbuki söz konusu servis talepleri, proxy tarafından servisin anlayacağı paketler haline getirilerek gönderilir. Benzer şekilde servisten gelen paketlerde proxy tarafından açılarak istemcideki çalışma zamanı(RunTime) nesnelerine devredilir. Bu konuda aşağıdaki şekil biraz daha aydınlatıcı bilgi verebilir.

Tabi burada proxy tiplerinin geliştirme zamanında eklenmesi gibi bir zorunluluk yoktur. Bazı uygulama çeşitlerinde örneğin Ajax temelli web sayfalarında söz konusu proxy tiplerine ait nesne örnekleri çalışma zamanında transparant olarak oluşturulup kullanılabilirler. Ajax tabanlı bir web istemcisi üzerinden bir Ado.Net Data Service' in kullanımı çokda kolay değildir. İşin özellikle benim açımdan keyifsizleştiği nokta istemci tarafı için javascript kodları döktürülmesi gerekliliğidir. Ajax tabanlı bir web formunda bir Ado.Net Data Service' inin nasıl kullanılabileceğini ilgili görsel dersten takip edebilirsiniz.

Peki biz bu yazımızda neler yapacağız? Aslında yukarıdaki listede yer almayan bir istemci uygulama geliştiriyor olacağız :) Tahmin edeceğiniz üzere bir Console uygulaması. Sonuçta amacımız bir istemci uygulamada basit REST taleplerinde bulunabilmek. Bunun içinde görsel detayların fazla olmadığı ve odağın tamamen koda kaydığı bir ortam kullanmamız öğrenmemiz açısından önemli olacaktır. İşte Console uygulaması seçmemizin(yada seçmemin) nedenide budur? Her zaman olduğu gibi basit bir Ado.Net Data Service' i geliştireceğiz. Senaryomuzda yine AdventureWorks veritabanını ve buradaki ProductSubCategory, Product tablolarını ele alacağız. Bu tablolar arasındaki bire çok ilişki istemci tarafında ilişkisel veri çekme işlemlerini analiz etmemizi sağlayacaktır. Ado.Net Data Service' in nasıl geliştirileceğini daha önceki notlarımızda ve görsel derslerimizde yeterince ele almıştık. Bu nedenle sadece EDM(Entity Data Model) grafiğinin, AdventureWorksServices.svc.cs kodlarının ve Solution içeriğininin aşağıdakilere benzer olmasına özen göstermeniz yeterli olacaktır.(Servis uygulamasının bir WCF Service şablonu olduğunu hatırlatalım.)

EDM Grafiği;

Solution İçeriği;

AdventureWorksServices.svc.cs içeriği;

using System;
using System.Data.Services;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel.Web;
using AdventureWorksModel;

public class AdventureWorksServices
    : DataService<AdventureWorksEntities>
{
    public static void InitializeService(IDataServiceConfiguration config)
    {
        config.SetEntitySetAccessRule("*", EntitySetRights.All);
    }
}

Sonrasında ise istemci Console uygulaması için gerekli proxy tiplerini üreteceğiz. Proxy üretimi için iki farklı seçeneğimiz bulunmaktadır. Bunlardan birisi WCF Servislerindende aşina oluğumuz Add Service Reference seçeneğidir. Bu seçeneği kullandığımızda karşımıza gelen dialog penceresinde Ado.Net Data Service adresinin yazılması yeterli olacaktır. Elbette geliştirilen örnek gereği Discover menüsünden Services in Solution seçeneği ele alınabilir. Nitekim servis ve istemci uygulamalarımız aynı solution içerisinde yer almaktadır.

Bu noktada normal bir WCF Service referansı eklerken karşımıza çıkan seçeneklerden tamamının aktif olmadığı hemen göze çarpmaktadır. Öyleki bir WCF servisi bu teknik ile eklenirken aktif olan Advanced düğmesi Ado.Net Data Service eklenirken aktif değildir. Buda olay bazlı asenkron ayarlamalar, erişim belirleyicileri(Internal veya Public) gibi bazı seçeneklerin kullanılamadığı anlamına gelmektedir. Ekleme işlemi sonrasında istemci uygulama tarafında servis ile ilişkili proxy referanslarının oluşturulduğu açık bir şekilde görülebilir.

Yine burada edmx uzantılı tip dikkati çekmektedir. Bu açıkçası bir XML içeriğidir ve istemci tarafına indirilmiş olan serileştirilebilir tipler ile ilgili eşleştirmeleri üzerinde taşımaktadır. Yine dikkat çekici noktalardan birisi istemci tarafı için bir config dosyası oluşturulmamış olmasıdır. Nitekim bu modelde EndPoint kullanımı söz konusu değildir. Zaten talepler basit HTTP metodları olacak şekilde servise ulaştırılmaktadır ki bu aşamada üretilen taşıyıcı(Container) sınıf devreye girmektedir(AdventureWorksEntities). Oluşturulan tiplere bakıldığında ise aşağıdaki sınıf diagramında yer alan açılımların oluştuğu görülür. Dikkat edileceği üzere servis tarafından sunulan entity tipleri istemci tarafındada oluşturulmuştur. Asıl yüklenici tip ise AdventureWorksEntities isimli DataServiceContext türevli sınıftır. Bu sınıf sayesinde CRUD operasyonlarının tamamı kolay bir şekilde istemci tarafında ele alınabilir.

Diğer Proxy üretme tekniği ise SvcUtil aracının Ado.Net Data Service' ler için geliştirilmiş olan versiyonu DataSvcUtil komut satırı programıdır. Komut satırından proxy üretimi için Visual Studio 2008 Command Prompt üzerinden DataSvcUtil aracının aşağıdaki resimde olduğu gibi kullanılması yeterli olacaktır. Çıktı AdventureProxy.cs isimli dosya içerisine yapılmaktadır. Uri parametresinden sonra ise Proxy üretimi için kaynak olan Ado.Net Data Service adresi verilmiştir.

Elbette elimizde Visual Studio 2008 gibi bir IDE olmadığı durumlarda proxy üretimi için DataSvcUtil aracını kullanmak gerekmektedir. Aksi durumda ise Add Service Reference seçeneği çok daha mantıklıdır. Sonuç olarak ben örneğimizde Add Service Reference seçeneğini ele aldım.

Artık ve nihayet istemci tarafındaki kodlarımızı geliştirmeye başlayabiliriz. Öncelikli olarak küçük bebek adımları ile başlamakta yarar vardır. Örneğin tüm ProductSubCategory listesini elde etmek istediğimizi düşünelim. Bu durumda kodlarımızı aşağıdaki gibi geliştirmemiz yeterli olacaktır.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Data.Services.Client;
using ClientApp.AdventureSpace;

namespace ClientApp
{
    class Program
    {
        static void Main(string[] args)
        {
            // Öncelikli olarak Proxy nesnesi örneklenir.
            // Parametre olarak Ado.Net Data Service' in URL bilgisi kullanılır.
            AdventureWorksEntities proxy = new AdventureWorksEntities(new Uri("http://localhost:1740/AdventureServices/AdventureWorksServices.svc"));

            // CreateQuery metodu parametre olarak Entity adını almaktadır.
            // Metodun döndürdüğü sonuç kümesi DataServiceQuery tipi ile ele alınabilir.
            // İstenirse var anahtar kelimeside göz önüne alınabilir. Her iki durumdada for döngüsü çalışacaktır.
            DataServiceQuery<ProductSubcategory> subCategories=proxy.CreateQuery<ProductSubcategory>("ProductSubcategory");
            // var subCategories = proxy.CreateQuery<ProductSubcategory>("ProductSubcategory");

            // Elde edilen sonuç kümesinin her bir elemanı ProductSubcategory sınıfı tipindendir.
            foreach (ProductSubcategory subCategory in subCategories)
            {
                //Her bir alt kategorinin Name ve ProductSubcategoryID özelliklerinin değerleri yazdırılır.
                Console.WriteLine("{0} : {1}",subCategory.ProductSubcategoryID,subCategory.Name);
            }
        }
    }
}

Bunun sonucu olarak aşağıdakine benzer bir ekran görüntüsü elde ederiz.

Şimdi işi biraz daha ilerletelim. Söz gelimi bu alt kategorilerin isimlerine göre tersten sıralı bir şekilde gelmesini istediğimizi düşünelim. Bu durumda CreateQuery metodu içerisinde ProductSubcategory?$orderby=Name desc şeklinde bir ifade kullanmamız kaçınılmazdır. Ne varki CreateQuery metodu sadece Entity adları ile çalışmaktadır ve bu nedenle ek parametreler alamaz. Dolayısıyla bu denemenin sonucu olarak aşağıdaki ekran görüntüsünde yer alan istisnaya(Exception) düşülür.

Öyleyse çare nedir? Parametrik bir sorgu söz konusu ise eğer bu durumda Execute metodunun kullanılması gerekmektedir. Bir başka deyişle kodları aşağıdaki şekilde değiştirmek yeterli olacaktır.

AdventureWorksEntities proxy = new AdventureWorksEntities(new Uri("http://localhost:1740/AdventureServices/AdventureWorksServices.svc"));

// Parametrik sorgu gönderimi için Execute metodu kullanılmalıdır.
var subCategories = proxy.Execute<ProductSubcategory>(new Uri("/ProductSubcategory?$orderby=Name desc", UriKind.Relative));

foreach (ProductSubcategory subCategory in subCategories)
{
    Console.WriteLine("{0} : {1}",subCategory.ProductSubcategoryID,subCategory.Name);
}

Program çalıştırıldığında aşağıdaki ekran görüntüsü elde edilir. Dikkat edileceği üzere alt kategoriler isimlerine göre tersten sıralı olacak şekilde elde edilmektedir.

Bu sorgular son derece basittir. İşi biraz daha karıştırmaya ne dersiniz? Örneğin A dan Z' ye sıralanmış alt kategorilerden ilk üçünü ve bunlara bağlı ürünleri elde etmek istediğimizi düşünelim. Bu noktada ProductSubcategory ve Product tipleri arasındaki ilişki(Association) son derece önemlidir. Bu sonuçları elde etmek için kodları ilk etapta aşağıdaki gibi geliştiririz.

AdventureWorksEntities proxy = new AdventureWorksEntities(new Uri("http://localhost:1740/AdventureServices/AdventureWorksServices.svc"));

var subCategories = proxy.Execute<ProductSubcategory>(new Uri("/ProductSubcategory?$orderby=Name&$top=3", UriKind.Relative));

foreach (ProductSubcategory subCategory in subCategories)
{
    Console.WriteLine("{0} : {1}",subCategory.ProductSubcategoryID,subCategory.Name);
    // O andaki alt kategoriye bağlı ürünleri gezmek için Product özelliğinden yararlanılır.
    foreach (Product product in subCategory.Product)
    {
        Console.WriteLine("\t {0}, {1}, {2}",product.ProductID.ToString(),product.Name,product.ListPrice.ToString());
    }
}

Ancak program çalıştırıldığında hiç beklenmedik bir sonuç elde edilir. Aynen aşağıdaki resimde olduğu gibi. Dikkat edileceği üzere sadece Alt kategori adları ve ID değerleri elde edilmiş bağlı olan ürün listeleri gelmemiştir.

Sebep son derece açıktır. Çünkü sadece ProductSubcategory içeriği servis tarafından istenmiştir. Bunlara bağlı Product nesne toplulukları alınmamıştır. Görsel derslerimizdende hatırlayacağınız üzere expand anahtar kelimesininin kullanılmasının sebebide budur. Dolayısıyla kodda aşağıda görüldüğü gibi küçük bir değişiklik yapmak gerekecektir.

var subCategories = proxy.Execute<ProductSubcategory>(new Uri("/ProductSubcategory?$orderby=Name&$top=3&$expand=Product", UriKind.Relative));

Bu haliyle kod çalıştırıldığında aşağıdaki sonuçlar elde edilir.

Makalenin bu kısımlarında içimden "böylesine önemli ve bir o kadar da güzide bir teknoloji içerisinde LINQ kullanılmaz mı?" diye geçirmiyor değilim. Tahmin ediyorumki sizlerinde bu yönde bazı beklentileri vardır. Öyleyse gelin kolları sıvayalım ve aşağıdaki kod parçasını göz önüne alalım.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Data.Services.Client;
using ClientApp.AdventureSpace;

namespace ClientApp
{
    class Program
    {
        static void Main(string[] args)
        {
            AdventureWorksEntities proxy = new AdventureWorksEntities(new Uri("http://localhost:1740/AdventureServices/AdventureWorksServices.svc"));

            var subCategories = from sc in proxy.ProductSubcategory
            orderby sc.Name descending
            select sc;
   
            foreach (ProductSubcategory subCategory in subCategories)
            {
                Console.WriteLine("{0} : {1}",subCategory.ProductSubcategoryID,subCategory.Name);
            }
        }
    }
}

Bu kez gördüğünüz gibi Execute yada CreateQuery gibi metodlar kullanmadık. Bunların yerine doğrudan bir LINQ sorgusu yazdık ve işte sonuç;

Aslında yazılan LINQ sorgusu istemci tarafında bir HTTP ifadesinin oluşturulmasına ve servise doğru gönderilmesine neden olmaktadır. Öyleki debug modda subCategories değişkenine bakıldığında aşağıdaki ekran görüntüsünde olduğu gibi bir QueryString ifadesi oluştuğu gözlemlenir.

Buna göre ProductSubcategory ve bunlara bağlı ürünlerin elde edilmesi için yazılmış olan kod parçasında LINQ ifadelerini aşağıdaki gibi kullanarak aynı sonuçların elde edilebileceği açık bir şekilde görülebilir.

AdventureWorksEntities proxy = new AdventureWorksEntities(new Uri("http://localhost:1740/AdventureServices/AdventureWorksServices.svc"));

// Take metodu ile A...Z ye sıralanmış listenin ilk 3 elemanı alınmış olunur.
var subCategories = (from sc in proxy.ProductSubcategory
orderby sc.Name
select sc).Take<ProductSubcategory>(3);


// Elde edilen alt kategoriler dolaşışır
foreach (ProductSubcategory subCategory in subCategories)
{
    Console.WriteLine("{0} : {1}",subCategory.ProductSubcategoryID,subCategory.Name);
    // O andaki alt kategoriye bağlı ürünlerin çekilmesi için LoadProperty metodu kullanılır. İkinci parametre ilişkinin taşındığı özellik adıdır.
    proxy.LoadProperty(subCategory, "Product");
    // Artık o andaki alt kategori için yüklenen Product satırları dolaşılabili
    foreach (Product product in subCategory.Product)
    {
        Console.WriteLine("\t{0} {1} {2}",product.ProductID.ToString(),product.Name,product.ListPrice.ToString());
    }
}

Bu kod parçasında tek dikkat edilmesi gereken nokta LoadProperty özelliğinin kullanımıdır. Nitekim bu özellik ile ilişkisel veriler yüklenmediği takdirde alt kategoriye bağlı ürün listeleri servis tarafında çekilmez.

NOT : İster Execute metodu olsun ister LINQ sorgusu olsun, ilişkisel özelliklerce taşınan verilerin çekilmesi için sırasıyla expand anahtar kelimesinin yada LoadProperty metodunun kullanılması gerekir.

Artık sizde farklı sorgulama örnekleri deniyerek istemci tarafında neler yapılabileceğini analiz edebilirsiniz. Görüldüğü üzere bir Ado.Net Data Service' in istemci tarafından ele alınması standart bir servis kullanımına çok benzemektedir. Proxy tipleri burada işi kolaylaştırmakla birlikte LINQ sorgularınında kullanılabiliyor olması kişisel görüşüme göre son derece önemlidir.

Böylece bugünkü ders notlarımızında sonuna gelmiş bulunuyoruz. Bu ders notlarımızda basit bir istemcinin nasıl geliştirilebileceğini incelemeye çalıştık. Konu ile ilişkili olaraktan ilgili görsel dersi takip etmenizi öneririm. Bir sonraki ders notlarımızda istemci tarafında CRUD(CreateReadUpdateDelete) operasyonlarının nasıl ele alınabileceğini analiz etmeye çalışacağız; ve eğer mümkün olursa özel LINQ Provider kullanılması halinde, servis tarafında Insert, Update, Delete oparasyonlarına olanak sağlamak için neler yapılması gerektiğine değiniyor olacağız. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

DevelopingAstoriaClient.rar (57,66 kb)