در
قسمت قبل با OData به صورت مختصر آشنا شدیم. در این قسمت به امکانات توکار OData و جزئیات query options پرداخته و همچنین قابلیتهای امنیتی این پروتکل را بررسی مینماییم.
در قسمت قبلی، config مربوط به OData و همچنین Controller و Crud مربوط به آن entity پیاده سازی شد. در این قسمت ابتدا سه موجودیت را به نامهای Product ، Category و همچنین Supplier، به صورت زیر تعریف مینماییم:
به این صورت مدلهای خود را تعریف کرده و طبق مقالهی قبلی، Controllerهای هر یک را پیاده سازی نمایید:
public class Supplier
{
[Key]
public string Key {get; set; }
public string Name { get; set; }
}
public class Category
{
public int ID { get; set; }
public string Name { get; set; }
public virtual ICollection<Product> Products { get; set; }
}
public class Product
{
public int ID { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
[ForeignKey("Category")]
public int CategoryId { get; set; }
public Category Category { get; set; }
[ForeignKey("Supplier")]
public string SupplierId { get; set; }
public virtual Supplier Supplier { get; set; }
}
پکیج Microsoft.AspNet.OData به تازگی ورژن 6 آن به صورت رسمی منتشر شده و شامل تغییراتی نسبت به نسخهی قبلی آن است. اولین نکتهی حائز اهمیت، Config آن است که به صورت زیر تغییر کرده و باید Optionهای مورد نیاز، کانفیگ شوند. در این نسخه DI نیز به Odata اضافه شده است:
public static void Register(HttpConfiguration config)
{
ODataModelBuilder odataModelBuilder = new ODataConventionModelBuilder();
var product = odataModelBuilder.EntitySet<Product>("Products");
var category = odataModelBuilder.EntitySet<Category>("Categories");
var supplier = odataModelBuilder.EntitySet<Supplier>("Suppliers");
var edmModel = odataModelBuilder.GetEdmModel();
supplier.EntityType.Ignore(c => c.Name);
config.Select(System.Web.OData.Query.QueryOptionSetting.Allowed);
config.MaxTop(25);
config.OrderBy(System.Web.OData.Query.QueryOptionSetting.Allowed);
config.Count(System.Web.OData.Query.QueryOptionSetting.Allowed);
config.Expand(System.Web.OData.Query.QueryOptionSetting.Allowed);
config.Filter(System.Web.OData.Query.QueryOptionSetting.Allowed);
config.Count(System.Web.OData.Query.QueryOptionSetting.Allowed);
//config.MapODataServiceRoute("ODataRoute", "odata", edmModel); // کانفیگ به صورت معمولی
config.MapODataServiceRoute("ODataRoute", "odata",
builder =>
{
builder.AddService(ServiceLifetime.Singleton, sp => edmModel);
builder.AddService<IEnumerable<IODataRoutingConvention>>(ServiceLifetime.Singleton, sp => ODataRoutingConventions.CreateDefault());
});
}
باید همهی Query optionهایی را که به آنها نیاز داریم، معرفی نماییم و فرض کنید که ProductsController و متد Get آن بدین صورت پیاده سازی شده باشد:
[EnableQuery]
public IQueryable<Product> Get()
{
return new List<Product>
{
new Product { Id = 1, Name = "name 1", Price = 11, Category = new Category {Id =1, Name = "Cat1" } },
new Product { Id = 2, Name = "name 2", Price = 12, Category = new Category {Id =2, Name = "Cat2" } },
new Product { Id = 3, Name = "name 3", Price = 13, Category = new Category {Id =3, Name = "Cat3" } },
new Product { Id = 4, Name = "name 4", Price = 14, Category = new Category {Id =4, Name = "Cat4" } },
}.AsQueryable(); ;
}
در این پروتکل به صورت توکار، optionهای زیر قابل استفاده است:
Description | Option |
بسط دادن موجودیت مرتبط | $expand |
فیلتر کردن نتیجه، بر اساس شرطهای Boolean ی | $filter |
فرمان به سرور که تعداد رکوردهای بازگشتی را نیز نمایش دهد(مناسب برای پیاده سازی server-side pagging ) | $count |
مرتب کردن نتیجهی بازگشتی | $orderby |
select زدن روی پراپرتیهای درخواستی | $select |
پرش کردن از اولین رکورد به اندازهی n عدد | $skip |
فقط بازگرداندن n رکورد اول | $top |
کوئریهای زیر را در نظر بگیرید:
در کوئری اول، فقط فیلدهای Id,Name از Products برگشت داده خواهند شد و در کوئری دوم، از 2 رکورد اول، صرفنظر میشود و از بقیهی آنها، فقط 3 رکورد بازگشت داده میشود:
/odata/Products?$select=Id,Name
/odata/Products?$top=3&$skip=2
/odata/Products?$count=true
در پاسخ کوئری فوق، تعداد رکوردهای بازگشتی نیز نمایش داده میشوند:
{@odata.context: "http://localhost:4516/odata/$metadata#Products", @odata.count: 4,…}
@odata.context:"http://localhost:4516/odata/$metadata#Products"
@odata.count:4
value:[{Id: 1, Name: "name 1", Price: 11, SupplierId: 0, CategoryId: 0},…]
0:{Id: 1, Name: "name 1", Price: 11, SupplierId: 0, CategoryId: 0}
1:{Id: 2, Name: "name 2", Price: 12, SupplierId: 0, CategoryId: 0}
2:{Id: 3, Name: "name 3", Price: 13, SupplierId: 0, CategoryId: 0}
3:{Id: 4, Name: "name 4", Price: 14, SupplierId: 0, CategoryId: 0}
در response، این مقادیر به همراه تعداد رکورد بازگشتی، نمایش داده میشوند که برای پیاده سازی paging مناسب است.
/odata/Products?$filter=Id eq 1
در کوئری فوق eq مخفف equal و به معنای برابر است و بجای آن میتوان از gt به معنای بزرگتر و lt به معنای کوچکتر نیز استفاده کرد:
/odata/Products?$filter=Id gt 1 and Id lt 3
/odata/Products?$orderby=Id desc
در کوئری فوق نیز به صورت واضح، بر روی فیلد Id، مرتب سازی به صورت نزولی خواهد بود و در صورت وجود نداشتن کلمه کلیدی desc، به صورت صعودی خواهد بود.
بسط دادن موجودیتهای دیگر نیز بدین شکل زیر میباشد:
/odata/Products?$expand=Category
برای اینکه چندین موجودیت دیگر نیز بسط داده شوند، اینگونه رفتار مینماییم:
/odata/Products?$expand=Category,Supplier
برای اینکه به صورت عمیق به موجودیتهای دیگر بسط داده شود، بصورت زیر:
/odata/Categories(1)?$expand=Products/Supplier
و برای اینکه حداکثر تعداد رکورد بازگشتی را مشخص نماییم:
[EnableQuery(PageSize = 10)]
محدود کردن Query Options به صورت زیر میتوانیم فقط optionهای دلخواه را فراخوانی نماییم. مثلا در اینجا فقط اجازهی Skip و Top داده شده است و بطور مثال Select قابل فراخوانی نیست:
[EnableQuery (AllowedQueryOptions= AllowedQueryOptions.Skip | AllowedQueryOptions.Top)]
برای اینکه فقط اجازهی logical function زیر را بدهیم (فقط eq):
[EnableQuery(AllowedLogicalOperators=AllowedLogicalOperators.Equal)]
برای اینکه Property خاصی در edm قابلیت نمایش نباشد، در config بطور مثال Price را ignore مینماییم:
var product = odataModelBuilder.EntitySet<Product>("Products");
product.EntityType.Ignore(e => e.Price);
برای اینکه فقط بر روی فیلدهای خاصی بتوان از orderby استفاده نمود:
[EnableQuery(AllowedOrderByProperties = "Id,Name")]
یک option به نام value$ برای بازگرداندن تنها آن رکورد مورد نظر، به صورت مجزا میباشد. برای اینکار بطور مثال متد زیر را به کنترلر خود اضافه کنید:
public System.Web.Http.IHttpActionResult GetName(int key)
{
Product product = Get().Single(c => c.Id == key);
return Ok(product.Name);
}
/odata/Products(1)/Name/$value
و حاصل کوئری فوق، مقداری بطور مثال برابر زیر خواهد بود و نه به صورت convention پاسخهای OData، فرمت بازگشتی "text/plain" خواهد بود و نه json:
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
DataServiceVersion: 3.0
Content-Length: 3
Ali
Attribute Convention هایی هم برای اعتبارسنجی پراپرتیها موجود است که نام آنها واضح تعریف شدهاند:
Description | Attribute |
اجازهی فیلتر زدن بر روی آن پراپرتی داده نخواهد شد | NotFilterable |
اجازهی مرتب کردن بر روی آن پراپرتی داده نخواهد شد | NotSortable |
اجازهی select زدن بر روی آن پراپرتی داده نمیشود | NotNavigable |
اجازهی شمارش دهی بر روی آن Collection داده نمیشود | NotCountable |
اجازهی بسط دادن آن Collection داده نمیشود | NotExpandable |
و همچنین [AutoExpand] به صورت اتوماتیک آن موجودیت مورد نظر را بسط میدهد.
بطور مثال کدهای زیر را در مدل خود میتوانید مشاهده نمائید:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
[NotFilterable, NotSortable]
public decimal Price { get; set; }
[ForeignKey(nameof(SupplierId))]
[NotNavigable]
public virtual Supplier Supplier { get; set; }
public int SupplierId { get; set; }
[ForeignKey(nameof(CategoryId))]
public virtual Category Category { get; set; }
public int CategoryId { get; set; }
[NotExpandable]
public virtual ICollection<TestEntity> TestEnities { get; set; }
}
فرض کنید پراپرتی زیر را به مدل خود اضافه کرده اید
public DateTimeOffset CreatedOn { get; set; }
حال کوئری زیر را برای فیلتر زدن، بر روی آن در اختیار داریم:
/odata/Products?$filter=year(CreatedOn) eq 2016
در اینجا فقط Product هایی بازگردانده میشوند که در سال 2016 ثبت شدهاند:
/odata/Products?$filter=CreatedOn lt cast(2017-04-01T04:11:31%2B08:00,Edm.DateTimeOffset)
کوئری فوق تاریخ مورد نظر را Cast کرده و همهی Product هایی را که قبل از این تاریخ ثبت شدهاند، باز میگرداند.
Nested Filter In Expand
/odata/Categories?$expand=Products($filter=Id gt 1 and Id lt 5)
همهی Categoryها به علاوه بسط دادن Product هایشان، در صورتیکه Id آنها بیشتر از 1 باشد
و یا حتی بر روی موجودیت بسط داده شده، select زده شود:
/odata/Categories?$expand=Products($select=Id,Name)
Custom Attribute
ضمنا به سادگی میتوان اتریبیوت سفارشی نوشت:
public class MyEnableQueryAttribute : EnableQueryAttribute
{
public override IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions)
{
// Don't apply Skip and Top.
var ignoreQueryOptions = AllowedQueryOptions.Skip | AllowedQueryOptions.Top;
return queryOptions.ApplyTo(queryable, ignoreQueryOptions);
}
}
روی هر متدی از کنترلر خود که اتریبیوت [MyEnableQuery] را قرار دهید، دیگر قابلیت Skip, Top را نخواهد داشت.
Dependency Injection در آخرین نسخهی OData اضافه شده است. بطور پیشفرض OData بصورت case-sensitive رفتار میکند. برای تغییر دادن آن در نسخههای قدیمی، Extension Methodی به نام EnableCaseSensitive وجود داشت. اما در نسخهی جدید شما میتوانید پیاده سازی خاص خود را از هر کدام از بخشهای OData داشته باشید و با استفاده از تزریق وابستگی، آن را به config برنامهی خود اضافه کنید؛ برای مثال:
public class CaseInsensitiveResolver : ODataUriResolver
{
private bool _enableCaseInsensitive;
public override bool EnableCaseInsensitive
{
get { return true; }
set { _enableCaseInsensitive = value; }
}
}
اینجا پیاده سازی از ODataUriResolver انجام شده و متد EnableCaseInsensitive به صورت جدیدی override و در حالت default مقدار true را برمیگرداند.
حال به صورت زیر آن را میتوان به وابستگیهای config برنامه، اضافه نمود:
config.MapODataServiceRoute("ODataRoute", "odata",
builder =>
{
builder.AddService(ServiceLifetime.Singleton, sp => edmModel);
builder.AddService<IEnumerable<IODataRoutingConvention>>(ServiceLifetime.Singleton, sp => ODataRoutingConventions.CreateDefault());
builder.AddService<ODataUriResolver>(ServiceLifetime.Singleton, sp => new CaseInsensitiveResolver()); // how enable case sensitive
});
در قسمت بعدی به Actionها و Functionها در OData میپردازیم.