Web API Cross-Domain & OData & GET,GET(ID),UPDATE
Web API bir çeşit webservisidir. Ama en büyük farkı HTTP tabanlı olmasıdır.Buda platformadan, browserdan ve dilden bağımsız olarak her yerde kullanılabilmesini sağlamıştır.
Bir mobile uygulamada ilgili productları verirken, bir televizyon uygulamasında o günkü akışı verebilmektedir.İstemcilere HTTP protokolünün Post, Put, Get, Delete gibi standart methodlara karşılık vermektedir.
Farkıl formatlarda xml,json gibi istemcilere cevap verebilmesi, ayrıca OData(Open Data Protocol) desteği sayesinde, URL bazlı parametreler ile filitreleme yapabilmesi Web Api’in bize getirdiği yeni özelliklerden birkaçıdır. Eskiden Restful dediğimiz servislerden Javascript ile cross domaine düşmeden data çekmek çok meşakatli ve yorucu bir işti. Bu işlem web api ile çok basitleşmiştir.Çünkü REST baseddir.
Bazen database’in tamamını dış bir sisteme açmak istemeyiz. Adapter Design Pattern’de olduğu gibi, görülmesine izin verdiğimiz kadarını dış dünyaya açabiliriz. Bunun için tek yapmamız gereken Web API’nin döndüreceği datamodel’e sınırlar koymaktan başka birşey değildir.
Şimdi isterseniz örneğimize geçelim. Öncelikle data modelelmiz olan product’ı aşşağıda görüldüğü gibi oluşturalım.
Models/product.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
using System; using System.Collections.Generic; using System.Linq; using System.Web; namespace WebApiBlog.Models { public class Product { public int ID { get; set; } public string Name { get; set; } public string Category { get; set; } public decimal Price { get; set; } } } |
Aşşağıda controller’ımuzu ApiController’dan türettik.Ve önceden yarattığımız data modelimizi manuel olarak doldurduk.Dikkat ederseniz GetAllProduct() methodunun üstüne [Queryable] attribute’u eklenmiştir.Amaç OData(Open Data Protocol) kullanılarak Url üzerinden filter yapmaktır.
OData’nun kullanılabilmesi için NuGet’den alttaki dosyanın indirilmesi gerekmektedir.
Manuel doldurduğumuz aşşağıdaki Product Listemizden dışarıya servis yaratırken Category’sini paylaşmak istemediğimizi planlıyalım. Bunu için viewmodelimizi doldururken category’i göz ardı ettik.Ve sonunda modelimizi model.AsQueryable() olarak döndürdük.Bir de performance amaçlı cache kullandık.Update işlemlerini göz önüne alarak 30 dakikalık bir cache oluşturduk.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
public class ProductsController : ApiController { List<Product> products = new List<Product>() { new Product{Name="Tampon",Category="Ek",ID=1,Price=15.9m}, new Product{Name="Far",Category="Light",ID=2,Price=600m}, new Product{Name="Kapı",Category="Ek",ID=3,Price=200.50m}, new Product{Name="Ayna",Category="Fr",ID=4,Price=50.30m}, new Product{Name="Tampon",Category="Fr",ID=5,Price=90.20m}, new Product{Name="Cam Filmi",Category="Pr",ID=6,Price=30.30m}, new Product{Name="Boya Koruma",Category="Pr",ID=7,Price=120.30m}, new Product{Name="Direksiyon Motoru",Category="En",ID=8,Price=270m} }; [Queryable] public IQueryable<Product> GetAllProduct() { List<Product> model = new List<Product>(); //Cache var context = HttpContext.Current; if (context != null) { if (context.Cache["Product"] == null) { context.Cache.Add("Product", products.OrderBy(pr=>pr.Name).ToList(), null, System.Web.Caching.Cache.NoAbsoluteExpiration, new TimeSpan(0, 30, 0), System.Web.Caching.CacheItemPriority.Default, null); } } // Bitti Cache List<Product> cac = (List<Product>)context.Cache["Product"]; foreach (var item in cac) { Product mod = new Product(); mod.ID = item.ID; mod.Name = item.Name; mod.Price = item.Price; model.Add(mod); } return model.AsQueryable(); } |
WebAPI servisinin farklı domainlerde çalışması için cross domin olması gerekmektedir.Bunun için NuGet’in Library Package Manager dan Package Manager Console’a :
Tabi henüz işimiz bitmedi.Projemizdeki /App_Start/WebApi.config içine aşşağıdaki kod satırı eklenmesi gerekmektedir.
config.EnableCors();
Ayrıca ProductsController class’ının başına ya da kullanılacak tüm methodların başına tek tek aşşağıda görülen attribute’un eklenmesi gerekir.
1 |
[EnableCors(origins: "*", headers: "*", methods: "*")] |
Bir de ID parametresi ile Get işelmi yapıldığı zamanki çağrılacak methodu yazalım.Gerçi istersek Odata Url $filter‘ile de bunu yapabiliriz.Arada tabi fark var:)
Aşşağıdaki örnekte görüldüğü gibi Linq Categori propertysini çıkarıp istenen ID’li Product’ı döndürelim.Burada yine cache kullanılmıştır.İlgili data gene 30dakkalık cache üzerinden alınmıştır.
Yukarıdaki örnekte bunu neden yapmamış diyen arkadaşlar için aşşağıda linq extension kullandım:) Bu arada merak edenler için Linq üzerine derinlemesine ayrı bir makale yazıcam.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public Product GetProduct(int id) { //Cache var context = HttpContext.Current; if (context != null) { if (context.Cache["Product"] == null) { context.Cache.Add("Product", products.OrderBy(pr=>pr.Name).ToList(), null, System.Web.Caching.Cache.NoAbsoluteExpiration, new TimeSpan(0, 30, 0), System.Web.Caching.CacheItemPriority.Default, null); } } // Bitti Cache List<Product> cac = (List<Product>)context.Cache["Product"]; return cac.Where(pr => pr.ID == id).Select(re => new Product { ID = re.ID, Name = re.Name, Price = re.Price }).FirstOrDefault(); } |
Şimdilik projemizi bu hali ile derleyip azure üzerine publish yapalım ki cross domain özelliğini de test edebilelim.
Şimdilik url’den bir request atalım: http://webapiblog.azurewebsites.net/api/products
Mesela her sayfada 2 kayıt olmak üzere 5.sayfa gösterilmek istenirse alttaki gibi bir url query yazılabilir.
http://webapiblog.azurewebsites.net/api/products?$top=2&$skip=5
Price 100 den aşşa olanlar için aşşağıdaki gibi bir url query yazılabilir:
http://webapiblog.azurewebsites.net/api/products?$filter=Price le 100
İsmi Cam ile başlıyan ürünler için aşşağıdaki gibi bir url query yazılabilir:
http://webapiblog.azurewebsites.net/api/products?xml=true&$filter=startswith(Name,’Cam’)
İsminde ‘or’ geçenler için aşşağıdaki gibi bir url query yazılabilir:
http://webapiblog.azurewebsites.net/api/products?xml=true&$filter=substringof(‘or’,Name)
İsme göre tersten sıralama için aşşağıdaki gibi bir url query yazılabilir:
http://webapiblog.azurewebsites.net/api/products?xml=true&$orderby=Name desc
Peki parametreye göre result’ı xml yada json nasıl yapabiliriz?
Global.asax’a aşşağıdaki kodu yazmamız yeterlidir:
1 2 |
GlobalConfiguration.Configuration.Formatters.XmlFormatter.MediaTypeMappings.Add(new QueryStringMapping( "xml", "true", "application/xml")); GlobalConfiguration.Configuration.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/html")); |
Yukarıdaki kodlar eklendiğinde, header tipini url’de xml=true parametresi bulunursa xml, yok ise json formatı oluşturularak sonuç döndürülür.
Şimdi gelelim 2 farklı sorgu biçimine. Ilki aşağıdaki gibidir:
http://webapiblog.azurewebsites.net/api/products?xml=true&$filter=ID eq 8
Diğeri:
http://webapiblog.azurewebsites.net/api/products/8?xml=true
Bu ikisi arasındaki fark nedir? İkiside aynı ID’si 8 olan ürünü döner. Esas fark biri yani odata filter kullanılan; tüm kayıtı önce çeker, sonra ID=8 olan resultı getirir ve Get() methodunu kullanır.
Diğer sadece ID’si 8 olan ürünü çeker.Ve Get(ID) methodunu kullanır.
Buraya kadar olan kısmı test amaçlı olarak bir test mvc projesi açalım.
Index sayfamıza alttaki gibi product namelerin geleceği bir dropbox ve detay bilgisinin dolacağı 4 textbox getirelim.
Örnek Ekran Url’i (İptal): http://testwebapiblogs.azurewebsites.net/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
<div id="body"> <br /><br /> <select id="products" class="target"></select><br /> <br /> <table> <tr> <td> ID: </td> <td> <input type="text" id="NO" /> </td> </tr> <tr> <td> Name: </td> <td> <input type="text" id="Name" /> </td> </tr> <tr> <td> Category: </td> <td> <input type="text" id="Category" /> </td> </tr> <tr> <td> Price: </td> <td> <input type="text" id="Price" /> </td> </tr> </table> </div> |
Aşşağıda görüldüğü gibi webAPI’ile çekilen ürün isimleri ilgili dropbox’a doldurulmuştur. Bu da bize cross domain’i aştığımızı göstermektedir.
1 2 3 4 5 6 7 8 9 10 |
<script src="~/Scripts/jquery-1.10.2.min.js"></script> <script type="text/javascript"> $(function () { $.getJSON('http://webapiblog.azurewebsites.net/api/products/', function (productsJsonPayload) { $('#products').append($('<option>').text('Seçiniz').attr('value', -1)); $(productsJsonPayload).each(function (i, item) { $('#products').append($('<option>').text(item.Name).attr('value', item.ID)); }); }); }); |
Aşşağıdaki scirpt’de dropbox’dan bir ürün seçildiği zaman detayı getirilmiştir:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
$(document).ready(function() { $(".target").change(function () { /* $.getJSON('http://webapiblog.azurewebsites.net/api/products?$filter=ID eq ' + $('#products option:selected').val(), function (data) { $("#NO").val(data[0].ID); $("#Name").val(data[0].Name); $("#Category").val(data[0].Category); $("#Price").val(data[0].Price); */ $.getJSON('http://webapiblog.azurewebsites.net/api/products/' + $('#products option:selected').val(), function (data) { $("#NO").val(data.ID); $("#Name").val(data.Name); $("#Category").val(data.Category); $("#Price").val(data.Price); }); }); }); |
Yukarıda görüldüğü üzere sonucu 2 farklı şekilde çekilmiştir.1.si odata filter kullanarak.2. side Get(int ID) methoduna yönelmek.Tabiki performans için Get(ID) methodu çok daha verimlidir.Çünkü tek bir kayıt çekmektedir. Odata url filter’da, önce tüm kayıt çekilir sonra ID değerine göre filter yapılır.
Dikkat edilirse Category bilgisi paylaşılmak istenmiyordu. Burada da görüldüğü üzere category bilgisi gelmemiştir.
Şimdi biraz’da güncellme üzerine bir örnek yapalım.Amaç Name veya Price için yapılan değişikliği 30’dakkalık cache’e yansıtmak.Önce gerekli WebAPI kodlarını oluşturalım.
Aşşağıda görüldüğü gibi değişen product’ın ID’sinden eski product çekilir. Çekilen bu product cache içindeki List<Product>’dan çıkarılır ve yerine değişen bu yeni product item konur.En sonunda da cache güncellenir ve işlem bitince kaydedilip ilgili mesaj döndürülür.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
public string UpdateProduct(Product product) { //Cache var context = HttpContext.Current; if (context != null) { if (context.Cache["Product"] == null) { context.Cache.Add("Product", products.OrderBy(pr=>pr.Name).ToList(), null, System.Web.Caching.Cache.NoAbsoluteExpiration, new TimeSpan(0, 30, 0), System.Web.Caching.CacheItemPriority.Default, null); } } // Bitti Cache List<Product> prd = (List<Product>)context.Cache["Product"]; Product item = prd.Single<Product>(pri => pri.ID == product.ID); prd.Remove(item); prd.Add(product); if (context != null) { if (context.Cache["Product"] != null) { context.Cache.Remove("Product"); } context.Cache.Add("Product", prd.OrderBy(pr=>pr.Name).ToList(), null, System.Web.Caching.Cache.NoAbsoluteExpiration, new TimeSpan(0, 30, 0), System.Web.Caching.CacheItemPriority.Default, null); } return "Kaydedilmiştir."; } |
Aşşağıda görüldüğü gibi bir de Index view’ı inceliyelim.Kaydet diye bir button koyulmuştur.
Değişen product değerlerini tekrardan doldurulduktan sonra WepAPI ile data json olarak post edildi. İşlem başarıldığında productları listeleyen dropdownlist sıfırlanıp tekrardan servis yardımı ile dolduruldu.Ve geri kalan tüm alanlar sıfırlandı.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
<input type=button id="saveButton()" onclick="save()" value="Kaydet" /> function save() { var product = { "ID" : $("#NO").val(), "Name" : $("#Name").val(), "Price" : $("#Price").val(), "Category" : "" }; $.ajax({ type: 'POST', url: 'http://webapiblog.azurewebsites.net/api/products/', data: JSON.stringify(product), contentType: "application/json; charset=utf-8", traditional: true, success: function (data) { $('#products').empty() $.getJSON('http://webapiblog.azurewebsites.net/api/products/', function (productsJsonPayload) { $('#products').append($('<option>').text('Seçiniz').attr('value', -1)); $(productsJsonPayload).each(function (i, item) { $('#products').append($('<option>').text(item.Name).attr('value', item.ID)); }); }); $("#NO").val(""); $("#Name").val(""); $("#Price").val(""); $("#Category").val(""); } }); }; |
- Resimde Far’ın orjinal hali görünmektedir.
- İsmi Far yerine Sağ Far olarak değiştirilmiştir.Fiyatı 600 den 545.5’e olarak değiştirilip kaydedildi.
- Güncellemeden sonra DropDownList’imizde Far =>Sağ Far olarak gözükmektedir.
- Sağ Far seçilince Name=>Sağ Far ve Price=545.5 şeklinde yeni giridiğimiz 30’dakkalık cahce’e kaydedilen değerler gösterilir.
Görüldüğü üzere WebApi birçok platforma, farklı formatlarda servis verebilmektedir.Url üzerinden query’e imkan vermesi, crossdomain’i kolaylıkla aşması ve HTTP tabanlı olması onu daha uzun süre göz önünde tutacak gibi görünüyor.
Geldik bir makalenin daha sonuna. Yeni bir makalede görüşmek üzere herkese hoşçakalın.
Not:Örnek WebAPI Url(İptal): http://webapiblog.azurewebsites.net/api/products?xml=true
Source Code: http://www.borakasmer.com/projects/WebApiBlog
Güzel bir makale. Ancak girişteki REST ile ilgili açıklama biraz kafa karıştırıcı. Önce restful Javascript ile data çekmek meşakatli ve yorucu deniliyor ardından web api bu basitleşmiştir çünkü REST tabanlıdır deniliyor. Bu durumda REST ‘in kendisinden çok REST kullanan Javascript sorunlu olduğu sonucu çıkıyor herhalde.
Wcf ile kullanılan rest servisler’de cross domain sorunu ve configuration çok karmaşık idi.WebAPI ile makalede de belirttiğim gibi basit birkaç adım ile kolaylıkla aşılabilmektedir.Restful demekle WebAPI kendisi altyapıda bu teknolojiyi kullanmaktadır.
İyi günler.
Hocam merhaba,
Elinize saglik,
Fakat webinar seklinde de bir uygulama gelistirme sansiniz olsa keske :)
Selam Yiğit;
Webinar değil de bir daha ki seminer de zaten webapi’yi de kapsayan bir konu hazırladım.
Zamanı gelince beklerim.
Hoşçakal.
Güzel makale, elinize sağlık.
Teşekkürler Emre. İşine yaradı ise ne mutlu bana…
Hocam elinize sağlık güzel bir paylaşım olmuş. Hocam bir sorum olacaktı, hazırladığımız bu WebApi’yi Azur değilde normal ftp lere nasıl yükleyeceğiz? Bu konuda bilgi verirseniz çok sevinirim.