CRUD with OData in ASPNETCORE 5 -ClientSide-Part VI

Nabin Kumar Jha
13 min readMar 13, 2022

--

In last 5 articles we have discuss the OData implementation in server side. In this article, we will learn how we can fetch the data from OData API based on the filter criteria selected by user and display the result on jQuery datatable. I will try to explain how to do sorting, searching, and the paging using jQuery datatable from the server-side which is very useful whenever we have to display a large number of records. There are number of different ways to achieve sorting, searching, and paging but here is how we can do it using the nuget package Simple.OData.Client.

DataTable allows all these functionality both on server side and client side. We will only focus on the server side. The Datable library uses the POST methods to send all the parameters of the datatable. For more details please visit https://datatables.net/

Although each of the param can be extracted using the Http.Request object .

var draw = Request.Form.GetValues("draw").FirstOrDefault();  
var start = Request.Form.GetValues("start").FirstOrDefault();
var length = Request.Form.GetValues("length").FirstOrDefault();
var sortColumn = Request.Form.GetValues("columns[" + Request.Form.GetValues("order[0][column]").FirstOrDefault() + "][name]").FirstOrDefault();
var sortColumnDir = Request.Form.GetValues("order[0][dir]").FirstOrDefault();
var searchValue = Request.Form.GetValues("search[value]").FirstOrDefault();

But I prefer to create c# classes to receive the params. These classes are created in separate library so that we can create a nuget package and use it any other Web Application that uses jQuery data table.

namespace DataTable
{
public abstract class SearchParameter<T>
{
[JsonProperty("draw")]
public int draw { get; set; }
[JsonProperty("columns")]
public TableColumn[] columns { get; set; }
[JsonProperty("order")]
public SortOrder[] order { get; set; }
[JsonProperty("start")]
public int start { get; set; }
[JsonProperty("length")]
public int length { get; set; }
[JsonProperty("search")]
public SearchText search { get; set; }
public string SortColumn => columns != null && order != null && order.Length > 0
? (columns[order[0].Column].Data + " " + order[0].dir)
: null;
public long RecordsFiltered { get; set; }
public long RecordsTotal { get; set; }
public List<T> Data { get; set; }
[JsonProperty("filterBy")]
public CustomFilterBy[] FilterBy { get; set; }
}
public class CustomFilterBy
{
[JsonProperty("entityName")]
public string EntityName { get; set; }
[JsonProperty("propertyName")]
public string PropertyName { get; set; }
[JsonProperty("propertyValue")]
public string PropertyValue { get; set; }
}
public class SearchText
{
[JsonProperty("value")]
public string value { get; set; }
[JsonProperty("regex")]
public bool regex { get; set; }
}
public class SearchValue
{
[JsonProperty("value")]
public string Text { get; set; }
[JsonProperty("regex")]
public bool IsRegex { get; set; }
}
public class SortOrder
{
[JsonProperty("column")]
public int Column { get; set; }
[JsonProperty("dir")]
public string dir { get; set; }
}
public class TableColumn
{
[JsonProperty("data")]
public string Data { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("searchable")]
public bool searchable { get; set; }
[JsonProperty("orderable")]
public bool orderable { get; set; }
[JsonProperty("search")]
public SearchValue search { get; set; }
}
}

Now in the web project we create a class that inherits from SearchParameter and can have custom filter fields.

public class ProductSearchParameter : SearchParameter<Product>
{
public IEnumerable<SelectListItem> ProductCategorySelectList { get; set; }
}

The web application acts as a bridge between the browser and API. It accept the request from the browser, creates the oData query and send it to API. The API execute the query and send back the result to web application. The web application return back the result to browser.

To deserialize the result returned by the ODATA API the following classes need to be created

public class ProductSearchResult : SearchResult<Product>
{
}

public abstract class SearchResult<T> : SearchParameter<T>
{
public string odatacontext { get; set; }
public int odatacount { get; set; }
public T[] value {get;set;}
}

Install the nuget package on the Web Project

In the _Layout.cshtml page update the Header element with following JS and CSS library source. You can use local www folder or CDN or both as per your wish.

Shared\_Layout.cshtml

<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>ECom Web | Starter</title>
<!-- Tell the browser to be responsive to screen width -->
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
<environment names="Development">
<link rel="stylesheet" href="~/lib/bootstrap/css/bootstrap.css" />
<link rel="stylesheet" href="~/lib/font-awesome/css/all.css" />
<link rel="stylesheet" href="~/lib/ionicons/css/ionicons.css" />
<link rel="stylesheet" href="~/lib/admin-lte/dist/css/adminlte.css" />
<link rel="stylesheet" href="~/css/site.css" />
<link rel="stylesheet" href="https://cdn.datatables.net/buttons/2.2.2/css/buttons.dataTables.min.css" />
<link rel="stylesheet" href="https://cdn.datatables.net/searchpanes/2.0.0/css/searchPanes.dataTables.min.css" />
<link rel="stylesheet" href="https://cdn.datatables.net/select/1.3.4/css/select.dataTables.min.css" />
</environment>
<environment names="Staging,Production">
<link rel="stylesheet" href="~/lib/bootstrap/css/bootstrap.min.css" asp-append-version="true" />
<link rel="stylesheet" href="~/lib/font-awesome/css/all.min.css" asp-append-version="true" />
<link rel="stylesheet" href="~/lib/ionicons/css/ionicons.min.css" asp-append-version="true" />
<link rel="stylesheet" href="~/lib/admin-lte/dist/css/adminlte.min.css" asp-append-version="true" />
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>
<environment names="Development">
<script src="~/lib/jquery/jquery.js" asp-append-version="true"></script>
<script src="~/lib/bootstrap/js/bootstrap.js" asp-append-version="true"></script>
<script src="~/lib/admin-lte/dist/js/adminlte.js" asp-append-version="true"></script>
<script src="~/lib/pace/pace.js" asp-append-version="true"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
<script src="~/lib/datatables/js/jquery.dataTables.js" asp-append-version="true"></script>
<script src="~/lib/datatables/js/dataTables.bootstrap4.js" asp-append-version="true"></script>
<script src="~/lib/sweetalert/dist/sweetalert-dev.js"></script>
<script src="https://cdn.datatables.net/buttons/2.2.2/js/dataTables.buttons.min.js"></script>
<script src="https://cdn.datatables.net/searchpanes/2.0.0/js/dataTables.searchPanes.min.js"></script>
<script src="https://cdn.datatables.net/select/1.3.4/js/dataTables.select.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.3/jszip.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.53/pdfmake.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.53/vfs_fonts.js"></script>
<script src="https://cdn.datatables.net/buttons/2.2.2/js/buttons.html5.min.js"></script>
<script src="https://cdn.datatables.net/buttons/2.2.2/js/buttons.print.min.js"></script>
</environment>
<environment names="Staging,Production">
<script src="~/lib/jquery/jquery.min.js" asp-append-version="true"></script>
<script src="~/lib/bootstrap/js/bootstrap.min.js" asp-append-version="true"></script>
<script src="~/lib/admin-lte/dist/js/adminlte.min.js" asp-append-version="true"></script>
<script src="~/lib/pace/pace.min.js" asp-append-version="true"></script>
<script src="~/js/site.min.js" asp-append-version="true"></script>
<script src="~/lib/datatables/js/jquery.dataTables.min.js" asp-append-version="true"></script>
<script src="~/lib/datatables/js/dataTables.bootstrap4.min.js" asp-append-version="true"></script>
<script src="~/lib/sweetalert/dist/sweetalert.min.js"></script>
</environment>
<script src="~/js/site.js"></script>
</head>

Along with sorting searching and pagination the datatable will also support the Create/Edit and Delete functionality as displayed in the image below

Controllers/Product

Let’s add a controller name Product on the Controllers folder.

public class ProductController : BaseController
{
private readonly ILogger<ProductController> _logger;
public IProductHttpClient _productHttpClient;
public IProductCategoryHttpClient _productCategoryHttpClient;
private readonly IMapper _mapper;
public ProductController(ILogger<ProductController> logger, IProductHttpClient productHttpClient, IProductCategoryHttpClient productCategoryHttpClient, IMapper mapper)
{
_logger = logger;
_productHttpClient = productHttpClient;
_productCategoryHttpClient = productCategoryHttpClient;
_mapper = mapper;
}
public async Task<IActionResult> Index()
{
var searchParameter = new ProductSearchParameter
{
ProductCategorySelectList = await PopulateProductCategorySelectList(0, true)
};
return View(searchParameter);
}
[HttpPost]
public async Task<JsonResult> GetProductList([FromBody] ProductSearchParameter param)
{
var result = await _productHttpClient.GetSearchResult(param);
return Json(result);
}
// GET: Products/Create
public async Task<IActionResult> Create()
{
var product = new ProductViewModel { CreatedDate = System.DateTime.Now};
product.ProductCategorySelectList = await PopulateProductCategorySelectList();
return PartialView("_Edit", product);
}
// GET: Products/Edit/5
public async Task<IActionResult> Edit(int? id)
{
var product = await _productHttpClient.GetById(id.Value);
product.ProductCategorySelectList = await PopulateProductCategorySelectList(product.ProductCategoryId);
return PartialView("_Edit", product);
}
private async Task<List<SelectListItem>> PopulateProductCategorySelectList(int productCategoryId = 0,bool addPleaseDefault=false)
{
var param = new ProductCategorySearchParameter { length = 100 };
var productCategories = await _productCategoryHttpClient.GetSearchResult(param);
return SelectedListHelper.GetProductCategorySelectList(productCategories.Data, productCategoryId, addPleaseDefault);
}
// POST: Products/Edit/5
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Save(int id, [Bind("Id,Name,SKU,Slug,IsFeatured,ImageUrl,CreatedDate,Description,Price,Rating,Brand,ReviewCount,StockCount,ProductCategoryId")] ProductViewModel productViewModel)
{
var action = id == 0 ? "Created" : "Updated";
if (!ModelState.IsValid)
{
return PartialView("_Edit", productViewModel);
}
var product = _mapper.Map<Product>(productViewModel);
productViewModel = id == 0 ? await _productHttpClient.Create(product) : await _productHttpClient.Update(product);
return Json(new { success= !productViewModel.HasError, message =$"The product {productViewModel.Name+" "+action} successfully", title = "Product " + action });
}
[HttpPost]
public async Task<IActionResult> Delete(int id)
{
await _productHttpClient.Delete(id);
return Json(new { message = "Product deleted successfully.", title = "Product Deleted" });
}
}

Views/Product/Index.cshtml

@model ECom.Web.Models.ProductSearchParameter
@{
ViewData["Title"] = "Product";
var parentDivModel = new ModalDialogViewModel { ParentDivId = "divProduct" };
}
<environment names="Development">
<link rel="stylesheet" href="~/lib/datatables/css/dataTables.bootstrap4.css" asp-append-version="true" />
<link rel="stylesheet" href="~/lib/sweetalert/dist/sweetalert.css" />
</environment>
<environment names="Staging,Production">
<link rel="stylesheet" href="~/lib/datatables/css/dataTables.bootstrap4.min.css" asp-append-version="true" />
<link rel="stylesheet" href="~/lib/sweetalert/dist/sweetalert.css" />
</environment>
<div class="box">
<div class="box-body table-responsive">
<!-- Create the drop down filter -->
<div id="divCategoryFilter">
<label id="lblCategotyFilter"> Category:</label>
<select style="width:60%" class="custom-select" id="filterProductCategoryId" asp-items="Model.ProductCategorySelectList"></select>
</div>
<table class="table table-bordered table-hover" id="productTableId" cellspacing="0" align="center">
<thead>
<tr>
<th>ID</th>
<th>SKU</th>
<th>slug</th>
<th>Name</th>
<th>Category</th>
<th>Edit</th>
<th>Delete</th>
</tr>
</thead>
</table>
</div>
</div>
<partial name="ModalDialog" model="@parentDivModel" />
@section Scripts{
<script src="~/js/Product/index.js"></script>
<script>
var baseUrl = "@Url.Content("~/")";
</script>
}

Javascript

Please create a file name index.js inside www/product folder.

function createProduct() {
var url = "/Product/Create";
showDialogWindow("divProduct", "frmProduct", url, "Create Product");
}
function editProduct(id) {
var url = "/Product/Edit?id=" + id;
showDialogWindow("divProduct", "frmProduct", url, "Edit Product");
}
function warnDeleteProduct(id, name) {
var message = `The product ${decodeURI(name)} will be deleted permanently.`;
displayDeleteAlert(message, deleteProduct, id);
}
function deleteProduct(id) {
var url = "/Product/Delete?id=" + id;
$.ajax({
url: url,
headers: {
'X-CSRF-Token': $('input:hidden[name="__RequestVerificationToken"]').val()
},
type: "POST",
success: function (data) {
swal("Deleted!", data.message, "success");
loadProduct();
}
});
return false;
}

$(function () {
loadProduct();
});
function loadProduct() {var tableOptions = {
searchDelay: 500,
"serverSide": true,
"filter": true,
"processing": true,
"ordering": true,
"deferRender": true,
"drawCallback": function () {
$("#dataTable_wrapper").children().eq(1).css("overflow", "auto");
},
language:
{
processing: "<div class=''><i class='fa fa-cog fa-spin site-loader-color'></i></div>",
search: "filter",
searchPlaceholder: "product name or desc"
},
"ajax": {
"type": "POST",
"url": baseUrl + "Product/GetProductList",
"datatype": "json",
"contentType": "application/json; charset=utf-8",
"headers": { 'RequestVerificationToken': $('#__RequestVerificationToken').val() },
"data": function (data) {
data.FilterBy = [];
data.FilterBy.push({ entityName: "ProductCategory", propertyName: "ProductCategoryId", propertyValue: $('#filterProductCategoryId').val() })// add custom filter condition
return JSON.stringify(data);
}
},
"columnDefs": [
{ "width": "5%", "targets": [5, 6] },
{
"targets": [0],
"visible": false,
"searchable": false
},
{
"targets": 5,
"data": "edit_link",
"searchable": false,
"render": function (data, type, row, meta) {
return "<button type='button' class='btn btn-primary ' onclick=editProduct('" + row.id + "');><i class='fa fa-edit'></i><span class='ml-1'>Edit</span></button>";
}
},
{
"targets": 6,
"data": "delete_link",
"searchable": false,
"render": function (data, type, row, meta) {
return "<button type='button' class='btn btn-danger' onclick=warnDeleteProduct('" + row.id + "','" + encodeURIComponent(row.name) + "'); ><i class='fa fa-trash'></i><span class='ml-1'>Delete</span></button>";
}
}
],
"columns": [
{ "data": "id" },
{ "data": "sku" },
{ "data": "slug" },
{ "data": "name" },
{ "data": "category" },
],
dom: "<'row'<'col-sm-2 col-md-1'<'#actionButtonContainer'>><'col-sm-4 col-md-5'Bl><'col-sm-3 col-md-4'<'#filterContainer'>><'col-sm-3 col-md-2'f>>" +
"<'row'<'col-sm-12'tr>>" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
buttons: [
'copy', 'csv', 'excel', 'pdf', 'print'
],
"order": [2, "asc"]
};
$('#productTableId').dataTable().fnDestroy();
var table = $('#productTableId').DataTable(tableOptions);
$("#actionButtonContainer").append($("<button class='btn btn-md dt-button' onclick='createProduct()'><i class='fa fa-plus'></i><span>&nbsp;Add</span></button>"));
//Take the category filter drop down and append it to the datatables_filter div.
//You can use this same idea to move the filter anywhere withing the datatable that you want.
$("#filterContainer").append($("#divCategoryFilter"));
//Set the change event for the Category Filter dropdown to redraw the datatable each time
//a user selects a new filter.
$("#filterProductCategoryId").change(function (e) {
table.draw();
});
}

The common logic to display the Modal popup is written in site.js

function displaySuccessMessage(title,message) {
var type = 'success';
displayMessage(title, message, type);
}
function displayErrorMessage(title,message) {
var type = 'error';
displayMessage(title, message, type);
}
function displayWarningMessage(title,message) {
var type = 'warning';
displayMessage(title, message, type);
}
function displayInfoMessage(title,message) {
var type = 'info';
displayMessage(title, message, type);
}
function displayMessage(title,message, type) {
swal(title, message, type);
}
function showDialogWindow(parentDivId, formId, url, title) {
$.ajax({
url: url,
headers: {
'X-CSRF-Token': $('input:hidden[name="__RequestVerificationToken"]').val()
},
type: "GET",
success: function (data) {
if (data.message) {
displayErrorMessage(title,data.message);
} else {
$(".modal-title").text(title);
$(".modal-body").html(data);
$("#" + parentDivId).modal("show");
//hack to get clientside validation working
if (formId !== '')
$.validator.unobtrusive.parse("#" + formId);
}
}
});
return false;
};
function submitModalForm(form, event) {
event.preventDefault();
event.stopImmediatePropagation();
var model = objectifyForm(form.serializeArray());
$.ajax({
url: $(form).attr('action'),
type: "POST",
headers: {
'X-CSRF-Token': $('input:hidden[name="__RequestVerificationToken"]').val()
},
data: model,
success: function (result) {
if (result.message) {
$(".btnClose").click();
displaySuccessMessage(result.title,result.message);
} else {
$(".modal-body").html(result);
}
}
});
}function objectifyForm(formArray) {//serialize data function
var propertyNames = [];
var returnArray = {};
for (var i = 0; i < formArray.length; i++) {
if (propertyNames.indexOf(formArray[i]['name']) === -1) {
returnArray[formArray[i]['name']] = formArray[i]['value'];
propertyNames.push(formArray[i]['name']);
}
}
return returnArray;
}
function displayDeleteAlert(message, callbackFunction, inputParam) {
swal({
title: 'Are you sure?',
text: message,
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Yes, delete it!'
},
function (isConfirm) {
if (isConfirm) {
callbackFunction(inputParam);
} else {
swal("Cancelled", "Delete is cancelled :)", "error");
}
});
}

Create ModalDialog.cshtml in Shared folder to display the Add/Edit form as popup.

@model ECom.Web.Models.ModalDialogViewModel
<div class="modal fade" id="@Model.ParentDivId" data-backdrop="static" tabindex="-1" role="dialog" aria-labelledby="staticBackdropLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title w-80"></h4>
<button type="button" class="close w-20" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
<span class="sr-only">Close</span>
</button>
</div>
<div class="modal-body">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary btnClose" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" onclick="$('#@Model.ParentDivId').find('form').submit();">Save changes</button>
</div>
</div>
</div>
</div>
public class ModalDialogViewModel
{
public string ParentDivId{ get; set; }
public string ChildDivId{ get; set; }
public string Title{ get; set; }
public string Style{ get; set; }
}

Create a partial view name _Edit.cshtml file that will be used for both Create and Edit view.

@model ECom.Web.Models.ProductViewModel
<form id="frmProduct" class="form-horizontal" role="form" asp-controller="Product" asp-action="Save">
<input type="hidden" asp-for="Id" />
<div class="row">
<div class="col-md-12">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
</div>
<div class="col-md-12">
<div class="form-group">
<label asp-for="SKU" class="control-label"></label>
<input asp-for="SKU" class="form-control" />
<span asp-validation-for="SKU" class="text-danger"></span>
</div>
</div>
<div class="col-md-12">
<div class="form-group">
<label asp-for="Slug" class="control-label"></label>
<input asp-for="Slug" class="form-control" />
<span asp-validation-for="Slug" class="text-danger"></span>
</div>
</div>
<div class="col-md-12">
<div class="form-group form-check">
<label class="form-check-label">
<input class="form-check-input" asp-for="IsFeatured" /> @Html.DisplayNameFor(model => model.IsFeatured)
</label>
</div>
</div>
<div class="col-md-12">
<div class="form-group">
<label asp-for="ImageUrl" class="control-label"></label>
<input asp-for="ImageUrl" class="form-control" />
<span asp-validation-for="ImageUrl" class="text-danger"></span>
</div>
</div>
<div class="col-md-12">
<div class="form-group">
<label asp-for="CreatedDate" class="control-label"></label>
<input asp-for="CreatedDate" class="form-control" />
<span asp-validation-for="CreatedDate" class="text-danger"></span>
</div>
</div>
<div class="col-md-12">
<div class="form-group">
<label asp-for="Description" class="control-label"></label>
<input asp-for="Description" class="form-control" />
<span asp-validation-for="Description" class="text-danger"></span>
</div>
</div>
<div class="col-md-12">
<div class="form-group">
<label asp-for="Price" class="control-label"></label>
<input asp-for="Price" class="form-control" />
<span asp-validation-for="Price" class="text-danger"></span>
</div>
</div>
<div class="col-md-12">
<div class="form-group">
<label asp-for="Rating" class="control-label"></label>
<input asp-for="Rating" class="form-control" />
<span asp-validation-for="Rating" class="text-danger"></span>
</div>
</div>
<div class="col-md-12">
<div class="form-group">
<label asp-for="Brand" class="control-label"></label>
<input asp-for="Brand" class="form-control" />
<span asp-validation-for="Brand" class="text-danger"></span>
</div>
</div>
<div class="col-md-12">
<div class="form-group">
<label asp-for="ReviewCount" class="control-label"></label>
<input asp-for="ReviewCount" class="form-control" />
<span asp-validation-for="ReviewCount" class="text-danger"></span>
</div>
</div>
<div class="col-md-12">
<div class="form-group">
<label asp-for="StockCount" class="control-label"></label>
<input asp-for="StockCount" class="form-control" />
<span asp-validation-for="StockCount" class="text-danger"></span>
</div>
</div>
<div class="col-md-12">
<div class="form-group">
<label asp-for="ProductCategoryId" class="control-label"></label>
<select asp-for="ProductCategoryId" class="form-control" asp-items="Model.ProductCategorySelectList"></select>
</div>
</div>
</div>
</form>
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
<script type="text/javascript">
$(function () {
$("#frmProduct").submit(function (event) {
submitModalForm($(this), event, loadProductTable);
});
});
</script>

Let’s now create RestClients to access the API.

public interface IBaseHttpClient<TRequestEntity,TResponseEntity, TSearchParam,TSearchResult> 
where TRequestEntity : class
where TResponseEntity : class
where TSearchParam : class
where TSearchResult : class
{
Task<TSearchResult> GetSearchResult(TSearchParam searchParameter);
Task<TResponseEntity> GetById(int id);Task<TResponseEntity> Create(TRequestEntity entity);
Task<TResponseEntity> Update(TRequestEntity entity);
Task Delete(int id);
}public interface IProductHttpClient : IBaseHttpClient<Product,ProductViewModel, ProductSearchParameter, ProductSearchResult>
{

}
public abstract class BaseHttpClient
{
private readonly HttpClient _httpClient;
protected IODataClient oDataClient;
private ApplicationParameters applicationParameters;
private readonly IMemoryCache _cacheAPIToken ;
public BaseHttpClient(IOptions<ApplicationParameters> config, HttpClient httpClient, IMemoryCache cacheAPIToken)
{
applicationParameters = config.Value;
_httpClient = httpClient;
_cacheAPIToken = cacheAPIToken;
var token = GetTokenToAccessOdataAPI().Result;
var setting = GetSettingWithToken(applicationParameters.ApiUrl, token);
oDataClient = new ODataClient(setting);
}
private async Task<string> GetTokenToAccessOdataAPI()
{
var cacheKey = ("api","token");
var token = _cacheAPIToken.Get<string>(cacheKey);
if(token is null)
{
_httpClient.DefaultRequestHeaders.Add(applicationParameters.ApiKeyName, applicationParameters.ApiKeyValue);
var result = await _httpClient.PostAsJsonAsync(applicationParameters.ApiUrl + "Authenticate", new
{
UserName = applicationParameters.ApiUserName,
Password = applicationParameters.ApiPassword
});
result.EnsureSuccessStatusCode();
var content = await result.Content.ReadAsStringAsync();
var user = JsonSerializer.Deserialize<ApplicationLoginViewModel>(content);
token= user.token;
_cacheAPIToken.Set(cacheKey, token, TimeSpan.FromHours(1));
}
return token;
}
private ODataClientSettings GetSettingWithToken(string url, string accessToken)
{
var clientSettings = new ODataClientSettings(new Uri(url));
clientSettings.BeforeRequest += delegate (HttpRequestMessage message)
{
message.Headers.Add("Authorization", "Bearer " + accessToken);
message.Headers.Add(applicationParameters.ApiKeyName, applicationParameters.ApiKeyValue);
};
return clientSettings;
}
}

The OData client allows us to choose the Property that is required hence minimizing the data flow between API and Web Application. To fetch the value of the Property of an associated object , we have to called the EXPAND method.

The Create mehod hits the POST endpoint, Update method hit the PATCH endpoint and the Delete method hits the DELETE end point of the API.

public class ProductHttpClient : BaseHttpClient, IProductHttpClient
{
private IMapper _mapper;
public ProductHttpClient(IMapper mapper, IOptions<ApplicationParameters> config, HttpClient httpClient, IMemoryCache memoryCache) : base(config, httpClient, memoryCache)
{
_mapper = mapper;
}
public async Task<ProductSearchResult> GetSearchResult(ProductSearchParameter param)
{
var annotations = new ODataFeedAnnotations();
var productCommand = oDataClient.For<Product>()
.Expand(x => x.ProductCategory)
.Select(x => new { x.Id, x.Name, x.ProductCategory, x.Price, x.SKU, x.Slug, x.StockCount })
.Top(param.length)
.Skip(param.start);
AddCustomFilterByCondtion(param, productCommand);
if (!string.IsNullOrEmpty(param.search?.value))
{
productCommand.Filter(x => x.Name.Contains(param.search.value) || x.Description.Contains(param.search.value));
}
productCommand = AddSortingCondition(param, productCommand);
var products = await productCommand.FindEntriesAsync(annotations);
var result = new ProductSearchResult
{
draw = param.draw,
Data = _mapper.Map<List<ProductViewModel>>(products.ToList()),
RecordsFiltered = annotations.Count ?? 0,
RecordsTotal = annotations.Count ?? 0
};
return result;
}
private static void AddCustomFilterByCondtion(ProductSearchParameter param, IBoundClient<Product> productCommand)
{
if (param.FilterBy.Length > 0)
{
if (param.FilterBy.Any(x => x.PropertyName == "ProductCategoryId"))
{
int.TryParse(param.FilterBy.First(x => x.PropertyName == "ProductCategoryId").PropertyValue, out int productCategoryId);
if (productCategoryId > 0)
{
productCommand.Filter(x => x.ProductCategoryId == productCategoryId);
}
}
}
}
private static IBoundClient<Product> AddSortingCondition(ProductSearchParameter param, IBoundClient<Product> productCommand)
{
if (param.order?.Length > 0)
{
var sortColumn = param.SortColumn.Split(" ");
if (sortColumn.Length > 1)
{
var sortColumnName = sortColumn[0];
if (sortColumnName.Equals("category"))
{
if (sortColumn[1] == "desc")
productCommand = productCommand.OrderByDescending(x => x.ProductCategory.Name);
else
productCommand = productCommand.OrderBy(x => x.ProductCategory.Name);
}
else
{
if (sortColumn[1] == "desc")
productCommand = productCommand.OrderByDescending(sortColumnName);
else
productCommand = productCommand.OrderBy(sortColumnName);
}
}
}
return productCommand;
}
public async Task<ProductViewModel> GetById(int id)
{
var product = await oDataClient.For<Product>("Product").Filter(x => x.Id == id).FindEntryAsync();
var result = _mapper.Map<ProductViewModel>(product);
return result;
}
public async Task<ProductViewModel> Create(Product entity)
{
var product = await oDataClient.For<Product>("Product")// This name "Product" must match with API entity name.
.Set(entity)
.InsertEntryAsync();
var result = _mapper.Map<ProductViewModel>(product);
return result;
}
public async Task<ProductViewModel> Update(Product entity)
{
var product = await oDataClient.For<Product>("Product")
.Key(entity.Id)
.Set(entity)
.UpdateEntryAsync();
var result = _mapper.Map<ProductViewModel>(product);
return result;
}
public async Task Delete(int id)
{
await oDataClient.For<Product>()
.Key(id)
.DeleteEntryAsync();
}
}

I will encourage you to clone the repository from GitHub and use it as boilerplate for your sample project. Please do create issues if you find any.

The full source code is available at GitHub

Support Me!
Buy Me A Coffee

For further reading please check the following links

https://github.com/simple-odata-client/Simple.OData.Client/wiki/Results-projection,-paging-and-ordering
https://www.codeproject.com/Articles/686240/12-reasons-to-consume-OData-feeds-using-Simple-ODa

--

--

Nabin Kumar Jha

Problem solver and Certified Multi Cloud Architect(Azure + AWS)