GraphQL’i beğendin mi? OData’yı dene!
OData (Open Data Protocol) en basit haliyle veri kaynaklarımızı bir Restful Api üzerinde, Http aracılığıyla -bir url ile- sorgulamamıza imkan veren bir protokoldür. Bu protokol sayesinde verilerimiz üzerinde filtreleme (filter), seçme (projection), bağlı alanları getirme (expand), sayfalama (top, skip) gibi fonksiyonları çok kolay bir şekilde kullanabiliriz. Bu yazımızda Restful Api yeteneklerini arttıran Open Data standardını ASP.NET Core ile inceleyeceğiz.
Öncelikle OData‘nın kullanılmadığı bir proje düşünelim. Tüm verileri çektiğimiz bir Get()
methodu olduğunu düşünelim, ileride yalnızca belirli alanların (ör.: yalnızca id ve name alanları) dönülmesi ,istenebilir, sayfalama veya çeşitli query filter işlemleri de gerçekleştirilmesi gerekebilir. Tüm bu ihtiyaçlara cevap verebilmek için farklı farklı endpointlerin yazılması gerekecek. OData kullandığımız takdirde biz bu işin yalnızca business logic tarafıyla ilgileniyor, gerisini OData’ya bırakıyoruz.
Uygulama
Yazıyı uzatıp şişirmemek adına projeye buradan erişebilirsiniz. İçerisinde Category
, Manufacturer
ve Vehicle
varlıklarının olduğu bir Api projesidir.
Öncelikle gerekli olan Nuget paketini kurarak işleme başlayalım.
Install-Package Microsoft.AspNetCore.OData
Daha sonra Startup.cs
dosyamızı açarak, ConfigureServices()
methodunda OData’yı bir servis olarak ekleyeceğimizi bildirmeliyiz.
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddOData();
// ...
}
Ardından Configure()
methodu içerisinde middleware olarak ekleyip; hangi varlıklarımızı OData’ya açacağmızı belirtmeliyiz.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// ...
app.UseEndpoints(endpoints =>
{
endpoints.MapODataRoute("ODataRoute", "odata", GetEdmModel());
endpoints.MapControllers();
});
// ...
}
private IEdmModel GetEdmModel()
{
var builder = new ODataConventionModelBuilder();
builder.EntitySet<Category>("Categories");
builder.EntitySet<Manufacturer>("Manufacturers");
builder.EntitySet<Vehicle>("Vehicles");
return builder.GetEdmModel();
}
Buraya kadar yaptıklarımızı kısaca özetleyelim. MapODataRoute()
methodu ile OData route’ini ODataRoute
ismi, “odata” url ön ekiyle (prefix) kullanacağımızı bildiriyoruz. Model olarak gönderdiğimiz GetEdmModel()
içerisinde ise; dışarı açacağımız EntiySet’leri bir isimle bildiriyoruz. Burada dikkat edilmesi gereken nokta bu isimler sonra oluşturacağımız controller ismi ile eşleşmesi gerektiğidir. (Ör.: Vehicles
için VehiclesController
, Manufacturers
için ManufacturersController
gibi)
İşlemleri tamamladıktan sonra Controller sınıflarımıza geçebiliriz. Şimdilik tüm controller sınıflarındaki kodlar aynı olacağı için yalnızca VehiclesController
sınıfını paylaşıyorum.
public class VehiclesController : ODataController
{
private readonly AppDbContext _dbContext;
public CategoriesController(AppDbContext dbContext)
{
_dbContext = dbContext;
}
[EnableQuery]
public IActionResult Get()
{
return Ok(_dbContext.Vehicles);
}
}
Sınıflarımızı ControllerBase
değil ODataController
abstract sınıfından inherit ettiğimize ve Get()
methodunu [EnableQuery]
ile işaretlediğimize dikkat edin. Bu Attribute
yardımıyla OData‘ya sorgulama özelliğinin çalışma zamanında aktif edildiğini söylüyoruz.
Daha ileriye gitmeden önce bir konuya açıklık getirmem gerekiyor. OData ile veri dönmeyi hedeflediğimiz endpointlerimizi IQueryable
olarak dönmemiz gerekiyor. Aksi halde, gönderdiğimiz sorgular IEnumerable
olarak ele alınıp önce verilerin tamamı çekilerek belleğe yazılacak ve ardından sorgulama işlemi gerçekleşecektir. Bu da büyük verilerin olduğu bir tabloda performans kaybına yol açacaktır. IQueryable
döndüğümüz takdirde gönderilen sorgular işlenerek bir sql sorgucuğuna dönüştürülecek ve bu sayede yalnızca ihtiyaç duydumuz verilerle çalışmış olacağız.
OData Metadata
Sorgu muhattabının hangi EntitySet
olduğunu ve bu EntitySet
içeriğini Metadata
özelliği sayesinde görebilir, bir nevi Swagger gibi düşünebiliriz. Bu özellik için $metadata
anahtar kelimesi (keyword) kullanılır.
https://localhost:44357/odata adresine çağrı yapalım.
{
"@odata.context": "https://localhost:44357/odata/$metadata",
"value": [
{
"name": "Categories",
"kind": "EntitySet",
"url": "Categories"
},
{
"name": "Manufacturers",
"kind": "EntitySet",
"url": "Manufacturers"
},
{
"name": "Vehicles",
"kind": "EntitySet",
"url": "Vehicles"
}
]
}
Gördüğünüz üzere dışarı açtığımız EntitySet’ler ile birlikte $metadata
bilgisi de döndü. Metadata bilgisini elde etmek için bir çağrı daha yapalım.
https://localhost:44357/odata/$metadata adresine çağrı attığımız zaman aşağıdaki çıktıyı görmeniz gerekmektedir.
https://localhost:44357/odata/$metadata#Vehicles adresiyle kullandığımız bir EntitySet
ve bağlı bulunan -navigation property- tüm EntitySet
meta bilgilerini de elde edebiliriz.
Buraya kadar her şey tamam gibi, artık OData‘nın güçlü yanlarını keşfedelim.
OData Query Options
Operatörlere geçmeden önce nasıl veri çekeceğimize de hızlıca bakalım.
https://localhost:44357/odata/manufacturers çağrısıyla tüm Manufacturer bilgisine erişebiliriz.
$select filtresi
Bu filtre yardımıyla belirttiğimiz EntitySet
üzerinden hangi alanların dönüleceği belirtilir. Bu filtreyi kullanmadan önce Startup.cs
içerisinde bu filtreyi kullanıma açtığımızı belirtiyoruz, aksi takdirde aşağıdaki hata mesajını alacağız.
The query specified in the URI is not valid. The property ‘…’ cannot be used in the $select query option.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseEndpoints(endpoints =>
{
endpoints.Select();
// ...
});
}
https://localhost:44357/odata/vehicles?$select=Id,Model adresine çağrı attığımız zaman yalnızca Vehicles
sınıfından yalnızca Id
ve Model
alanlarının döndüğünü göreceğiz.
$expand filtresi
Bu filtre yardımıyla EntitySet ve bu EntitySet’e bağlanmış navigation propertyleri de sorguyla birlikte getirmek için kullanılır. Yine bu filtreyi kullanıma açmamız gerekmektedir.
endpoints.Select().Expand();
https://localhost:44357/odata/vehicles?$expand=Manufacturer adresine çağrı attığımızda Vehicle
sınıfının yanında Manufacturer
bilgisinin de döndüğünü göreceksiniz.
$expand
filtresiyle birlikte Manufacturer
sınıfı içerisinde $select
filtresini kullanmak için: https://localhost:44357/odata/vehicles?$expand=Manufacturer($select=Id,Name) adresine istek atmamız yeterli.
Category
sınıfı üzerinden Vehicle
sınıflarını ve bağlı olduğu Manufacturer
bilgisini çekmek için ise: https://localhost:44357/odata/categories?$expand=Vehicles($select=Id,Model;$expand=Manufacturer) adresine istek atmamız yeterli. Burada parantez içerisine girdiğimiz zaman filtreleri “;” ile ayırdığımıza dikkat edin.
$orderby filtresi
Bu filtre yardımıyla isminden de anlaşılacağı üzere belirtilen bir propery üzerinde sıralama işlemi gerçekleştiriyoruz. Yine bu filtreyi kullanıma açmamız gerekmektedir.
endpoints.Select().Expand().OrderBy();
Vehicles
sınıfından Id
, Model
ve CreatedOn
alanlarını seçerek CreatedOn
üzerinde azalan (descending) sıralama işlemi gerçekleştirelim.
https://localhost:44357/odata/vehicles?$select=Id,Model,CreatedOn&$orderby=CreatedOn desc adresine istek attığımız zaman aşağıdaki gibi bir veri seti elde etmeliyiz.
Ayrıca ikinci bir sıralamada bulunmak istiyorsak $orderby
filtresini $orderby=CreatedOn desc,Model asc
şeklinde belirtmemiz yeterli.
$top filtresi
Seçtiğimiz dizinden kaç eleman almak istediğimizi belirttiğimiz filtredir, yine bu özellik aktif edilmelidir. Bu özellik aktif edilirken en fazla kaç eleman seçilebileceğine dair boş geçilebilir bir parametre almaktadır, bu alanı bir değer vererek aktif etmekte fayda var, aşağıda en fazla 100 kayıt seçilebileceği belirtilmiştir.
endpoints.Select().Expand().OrderBy().MaxTop(100);
Vehicles
sınıfından yine $select
ve $orderby
filtreleri gerçeklendikten sonra üstten ilk 3 kaydı seçmek için https://localhost:44357/odata/vehicles?$select=Id,Model,CreatedOn&$orderby=CreatedOn desc&$top=3 adresine istek atılması yeterlidir.
$skip filtresi
Seçtiğimiz dizinden kaç elemanın atlanacağını belirttiğimiz filtredir, $top
filtresi ile birlikte kullanıldığında sayfalama (paging) özelliği kazandırılmış olur. Bu özellik için bir aktivasyon söz konusu değildir, varsayılan olarak gelmektedir.
https://localhost:44357/odata/vehicles?$select=Id,Model,CreatedOn&$orderby=CreatedOn desc&$top=3&$skip=3 adresine istek attığımızda tarihe göre azalan sırada sıralanmış ve üstten ilk 3 kaydın atlanarak sonraki 3 kaydın seçildiğini görürüz.
Sayfalamayı bir alternatif olarak [EnableQuery]
içerisinden de yapabiliriz. Bu durumda mevcut kodumuzu [EnableQuery(PageSize = 2)]
şeklinde değiştirmemiz yeterli. Bu durumda yalnızca $skip
filtresini kullanmamız yeterli olacaktır, ayrıca dönen veri seti içerisine @odata.nextLink
isminde bir düğüm eklendiğine de dikkat edelim, bir sonraki sayfalama verisi olduğu sürece sonraki sayfa bilgisi bu düğüm içerisinden elde edilebilir.
$count filtresi
İsminden de anlaşılacağı üzere filtreleme sonucu toplam kaç kaydın olduğu bilgisini verir. Yine bu özelliği aktif etmemiz gerekmektedir.
endpoints.Select().Expand().OrderBy().MaxTop(100).Count();
https://localhost:44357/odata/vehicles?$skip=2&$top=2&$orderby=Id desc&$count=true adresine istek attığımız zaman toplam kayıt sayısının @odata.count
içerisinde olduğu görülecektir. Ayrıca kayıt bilgisi gelsin istemiyorsak $count=false
ile bu işlemi gerçekleştirebiliriz.
Category
verisi üzerinden Vehicles
verilerini de çektiğimizi düşünelim, eğer Vehicles
verisine ilişkin $count
bilgisini istiyorsak bu işlemi çok basit bir şekilde gerçekleştirebiliriz.
https://localhost:44357/odata/categories?$expand=Vehicles($count=true)&$count=true adresine istek attığımızda Vehicles
düğümü altında [email protected]
şeklinde toplam kayıt sayısının dönüldüğü görülecektir.
$filter filtresi
OData‘nın asıl güçlü olduğu ve bir çok özellik sunan son filtresine geldik. Burada üç tür filtrelemeyi ele alacağız; mantıksal, aritmetiksel ve fonksiyonlar. İsminden de anlaşılacağı üzere dizinleri filtrelemeye yarar, filtre yine aktif edilmelidir.
endpoints.Select().Expand().OrderBy().MaxTop(100).Count().Filter();
Mantıksal Operatörler
OData bize $filter
filtresi ile birlikte aşağıdaki mantıksal operatörleri sunar.
- eq (equal): $filter=Name eq ‘Bmw’
- ne (not equal): $filter=Name ne ‘Bmw’
- gt (greater than): $filter=Name gt ‘Mazda’
- ge (greater than or equal): $filter=Name ge ‘Toyota’
- lt (lower than): $filter=Name lt ‘Volvo’
- le (lower than or equal): $filter=Name le ‘Volvo’
- or: $filter=Name eq ‘Bmw’ or Name eq ‘Volvo’
- and: $filter=Name eq ‘Bmw’ and Year lt 2010
Beygir gücü 100’den büyük Vehicle verilerini listelemek için https://localhost:44357/odata/vehicles?$filter=Engine gt 100 adresini kullanabiliriz ya da tarihi 1 Ocak 2020 tarihinden büyük olan kayıtları https://localhost:44357/odata/vehicles?$filter=CreatedOn ge 2020-01-01 adresiyle süzerek listeleyebiliriz.
Aritmetiksel Operatörler
OData bize $filter filtresi ile birlikte aşağıdaki aritmetik operatörleri sunar.
- add (addition): $filter=Year add 2 eq 2021
- sub (subtracts): $filter=Year sub 2 eq 2019
- mul (multiples): $filter=Doors mul 2 eq 10
- div (divides): $filter=Doors div 2 eq 2
- mod (modulo): $filter=Doors mod 5 eq 0
Canonical Fonksiyonlar
OData bize $filter filtresi ile birlikte kullanabileceğimiz aşağıdaki canonical fonksiyonları sunar, kullanım şekilleri yanlarında verilmiştir.
- substringof (string, string): $filter=substringof(‘Bmw’, Name) eq true
- endswith (string, string): $filter=endswith(BodyType, ‘Coupe’)
- startswith (string, string): $filter=startswith(Name, ‘Li’)
- length (string): $filter=length(Name) eq 7
- indexof (string, string): $filter=indexof(Name, ‘Toy’) eq 0
- replace (string, string, string): $filter=replace(Model, ‘ ‘, ”) eq ‘1Series’
- substring (string, int): $filter=substring(Name, 1) eq ‘azda’
- tolower (string): $filter=tolower(Name) eq ‘volvo’
- toupper (string): $filter=toupper(Name) eq ‘BMW’
- trim (string): $filter=length(trim(Name)) eq length(Name)
- concat (string, string): $filter=concat(concat(Generation, ‘, ‘), Model) eq ‘XC90 II, XC90’
- year (datetime): $filter=year(CreatedOn) eq 2020
- month (datetime): $filter=month(CreatedOn) eq 5
- day (datetime): $filter=day(CreatedOn) eq 7
- hour (datetime): $filter=hour(CreatedOn) eq 2
- minute (datetime): $filter=minute(CreatedOn) eq 59
- second (datetime): $filter=second(CreatedOn) eq 59
- round (double): $filter=round(Weight) eq 2000
- floor (double): $filter=floor(Weight) eq 1000
- ceiling (double): $filter=ceiling(Freight) eq 2000
- isof (type): $filter=isof(Api.Models.Vehicle)
- cast (type)
Yukarıda bahsettiğimiz operatörleri parantez operatörüyle birbirlerine bağlayabileceğinizi veya gruplayabileceğinizi de unutmayın.
OData ile ilgili olarak diğer yazılarıma da göz atabilirsiniz.