Using Angular Kendo Grid with Elastic Search and ASP.NET Core

There was a need for using a Kendo Grid in an Angular 5 website where the backing store for the data was Elastic Search. Utilizing the filtering on local data was simple enough but for the needs of filtering there needed to be server side integration. The server was running ASP.NET Core.

To get started create a view and view model for Angular to expose the grid.

import { Component } from '@angular/core';
import { process, State } from '@progress/kendo-data-query';
import { sampleProducts } from './products';
import {
GridComponent,
GridDataResult,
DataStateChangeEvent
} from '@progress/kendo-angular-grid';
@Component({
selector: 'my-app',
template: `
<kendo-grid
[data]="gridData"
[pageSize]="state.take"
[skip]="state.skip"
[sort]="state.sort"
[filter]="state.filter"
[sortable]="true"
[pageable]="true"
[filterable]="true"
(dataStateChange)="dataStateChange($event)">
<kendo-grid-column field="ProductID" title="ID" width="40" [filterable]="false">
</kendo-grid-column>
<kendo-grid-column field="ProductName" title="Product Name">
</kendo-grid-column>
<kendo-grid-column field="FirstOrderedOn" title="First Ordered On" width="240" filter="date" format="{0:d}">
</kendo-grid-column>
<kendo-grid-column field="UnitPrice" title="Unit Price" width="180" filter="numeric" format="{0:c}">
</kendo-grid-column>
<kendo-grid-column field="Discontinued" width="120" filter="boolean">
<ng-template kendoGridCellTemplate let-dataItem>
<input type="checkbox" [checked]="dataItem.Discontinued" disabled/>
</ng-template>
</kendo-grid-column>
</kendo-grid>
`
})
export class AppComponent {
public state: State = {
skip: 0,
take: 5,
// Initial filter descriptor
filter: {
logic: 'and',
filters: [{ field: 'ProductName', operator: 'contains', value: 'Chef' }]
}
};
public gridData: GridDataResult = process(sampleProducts, this.state);
public dataStateChange(state: DataStateChangeEvent): void {
this.state = state;
this.gridData = process(sampleProducts, this.state);
}
}

view raw
app.component.ts
hosted with ❤ by GitHub

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { HttpClientModule } from '@angular/common/http';
import { GridModule } from '@progress/kendo-angular-grid';
import { AppComponent } from './app.component';
@NgModule({
bootstrap: [
AppComponent
],
declarations: [
AppComponent
],
imports: [
BrowserModule,
BrowserAnimationsModule,
HttpClientModule,
GridModule
]
})
export class AppModule { }

view raw
app.module.ts
hosted with ❤ by GitHub

import { AppModule } from './app.module';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
const platform = platformBrowserDynamic();
platform.bootstrapModule(AppModule);

view raw
main.ts
hosted with ❤ by GitHub

To wire the view and view model to the server side data, there needs to be an Angular HTTP service and an ASP.NET Core Controller. The controller needs to be able to accept the filter and paging options of the grid as the user changes them and react to them server side. To accomplish this, some objects need to be created to handle the request:

First, the filter object, which is changed whenever a new filter is selected or is cleared; must be mapped to a C# object that can be serialized. The structure of the Kendo Grid filter is as such:

filter: {
      logic: 'and',
      filters: [{ field: 'ProductName', operator: 'contains', value: 'Chef' }]
}

To make that object transportable to C#, lets create a POCO:

public class CompositeFilterDescriptor
{
[JsonProperty("logic")]
public string Logic { get; set; }
[JsonProperty("filters")]
public ICollection<CompositeFilterDescriptor> Filters { get; set; }
[JsonProperty("field")]
public string Field { get; set; }
[JsonProperty("operator")]
public string Operator { get; set; }
[JsonProperty("value")]
public object Value { get; set; }
[JsonProperty("ignoreCase")]
public bool? IgnoreCase { get; set; }
}

public class Page
{
public int Skip { get; set; }
public int Take { get; set; }
}

view raw
Page.cs
hosted with ❤ by GitHub

public class RequestDto
{
public CompositeFilterDescriptor Filters { get; set; }
public Page Page { get; set; }
}

view raw
RequestDto.cs
hosted with ❤ by GitHub

Now lets create an ASP.NET Core controller endpoint for our Filter.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Nest;
public class GridController : Controller
{
private readonly ElasticClient _elasticClient;
public GridController()
{
//Initalize ElasticClient
}
[HttpPost]
public async Task<IActionResult> PostRequest([FromBody]RequestDto request)
{
CompositeFilterMapper compositeFilterMapper = new CompositeFilterMapper();
//Grab a page from elastic
selector = search =>
search.Index("your-index")
.From(request.Page.Skip)
.Size(request.Page.Take)
.Query(q =>
compositeFilterMapper.GetQueryContainer(q));
var searchResponse = _elasticClient.Search(selector);
return Ok(new ResponseDto
{
Results = searchResponse.Documents,
Count = searchResponse.Total
});
}
}

view raw
GridController.cs
hosted with ❤ by GitHub

public class ResponseDto<T>
{
public IEnumerable<T> Results { get; set; }
public long Count { get; set; }
}

view raw
ResponseDto.cs
hosted with ❤ by GitHub

The only thing missing now is the query to work is the CompositeFilterMapper.

class CompositeFilterMapper<T>
{
private readonly CompositeFilterDescriptor _compositeFilterDescriptor;=
public CompositeFilterMapper(CompositeFilterDescriptor compositeFilterDescriptor)
{
_compositeFilterDescriptor = compositeFilterDescriptor;
}
public QueryContainer GetQueryContainer(QueryContainerDescriptor<ElasticDataUpload<T>> query)
{
return ApplyQuery(new[] {_compositeFilterDescriptor}, _compositeFilterDescriptor.Logic, query);
}
private QueryContainerDescriptor<T> ApplyQuery(
IEnumerable<CompositeFilterDescriptor> filterFilters, string filterOperator,
QueryContainerDescriptor<T> query)
{
List<QueryContainerDescriptor<T>> innerQueries =
new List<QueryContainerDescriptor<T>>();
foreach (var filter in filterFilters)
{
if (filter.Filters != null && filter.Filters.Any())
{
innerQueries.Add(ApplyQuery(filter.Filters, filter.Logic, query));
}
else
{
innerQueries.Add((QueryContainerDescriptor<T>)CreateQuery(filter.Field, filter.Operator, filter.IgnoreCase, filter.Logic, filter.Value, query));
}
}
if (filterOperator == "and")
{
return (QueryContainerDescriptor<T>)query.Bool(b => b.Must(innerQueries.Cast<QueryContainer>().ToArray()));
}
else if (filterOperator == "or")
{
return (QueryContainerDescriptor<T>) query.Bool(b => b.Should(innerQueries.Cast<QueryContainer>().ToArray()));
}
else
{
throw new ArgumentOutOfRangeException();
}
}
private QueryContainer CreateQuery(string filterField,
string filterOperator,bool? filterIgnoreCase, string filterLogic, object value,
QueryContainerDescriptor<T> query)
{
var type = MapToType(filterField);
var propertyExpression = MapToProperty(filterField);
return CreateQuery(query,type, propertyExpression, filterOperator, value);
}
private QueryContainer CreateQuery(QueryContainerDescriptor<T> query,
Type fieldType, Expression<Func<T, object>> propertyExpression,
string filterOperator, object value)
{
if (fieldType == typeof(String))
{
return CreateStringFilter(query,propertyExpression, filterOperator, value);
}
else if (fieldType == typeof(DateTime))
{
return CreateDateTimeFilter(query, propertyExpression, filterOperator, value);
}
else
{
throw new ArgumentOutOfRangeException();
}
}
private QueryContainer CreateDateTimeFilter(QueryContainerDescriptor<ElasticDataUpload<SystemLog>> query,
Expression<Func<ElasticDataUpload<SystemLog>, object>> propertyExpression, string filterOperator,
object value)
{
switch (filterOperator)
{
case "eq":
return query.DateRange(d => d.Field(propertyExpression)
.GreaterThanOrEquals(DateMath.Anchored((DateTime)value))
.LessThanOrEquals(DateMath.Anchored((DateTime)value)));
case "neq":
return query.DateRange(d => d.Field(propertyExpression)
.GreaterThanOrEquals(DateMath.Anchored((DateTime)value))
.LessThanOrEquals(DateMath.Anchored((DateTime)value)));
case "isnull":
throw new ArgumentException("Unable to apply less than to date and time queries");
case "isnotnull":
throw new ArgumentException("Unable to apply less than to date and time queries");
case "lt":
return query.DateRange(d => d.Field(propertyExpression).LessThan(DateMath.Anchored((DateTime)value)));
case "lte":
return query.DateRange(d => d.Field(propertyExpression).LessThanOrEquals(DateMath.Anchored((DateTime)value)));
case "gt":
return query.DateRange(d => d.Field(propertyExpression).GreaterThan(DateMath.Anchored((DateTime)value)));
case "gte":
return query.DateRange(d => d.Field(propertyExpression).GreaterThanOrEquals(DateMath.Anchored((DateTime)value)));
case "startswith":
throw new ArgumentException("Unable to apply less than to date and time queries");
case "endswith":
throw new ArgumentException("Unable to apply less than to date and time queries");
case "contains":
throw new ArgumentException("Unable to apply less than to date and time queries");
case "doesnotcontain":
throw new ArgumentException("Unable to apply less than to date and time queries");
case "isempty":
throw new ArgumentException("Unable to apply less than to date and time queries");
case "isnotempty":
throw new ArgumentException("Unable to apply less than to date and time queries");
default:
throw new ArgumentOutOfRangeException();
}
}
private QueryContainer CreateStringFilter(QueryContainerDescriptor<T> query,
Expression<Func<T, object>> propertyExpression,
string filterOperator, object value)
{
switch (filterOperator)
{
case "eq":
return query.QueryString(qs => qs.Fields(fs => fs.Field(propertyExpression)).Query((string)value));
case "neq":
return query.Bool(b => b.MustNot(x => x.QueryString(qs => qs.Fields(fs => fs.Field(propertyExpression)).Query((string)value))));
case "isnull":
return query.QueryString(qs => qs.Fields(fs => fs.Field(propertyExpression)).Query(null));
case "isnotnull":
return query.Bool(b => b.MustNot(x => x.QueryString(qs => qs.Fields(fs => fs.Field(propertyExpression)).Query(null))));
case "lt":
throw new ArgumentException("Unable to apply less than to string queries");
case "lte":
throw new ArgumentException("Unable to apply less than equal to string queries");
case "gt":
throw new ArgumentException("Unable to apply greater than to string queries");
case "gte":
throw new ArgumentException("Unable to apply greater than equal to string queries");
case "startswith":
return query.Bool(b => b.Should(x => x.Term(propertyExpression, value)));
case "endswith":
return query.Bool(b => b.Should(x => x.Term(propertyExpression, value)));
case "contains":
return query.Match(qs => qs.Field(propertyExpression).Query((string)value).Operator(Operator.And));
case "doesnotcontain":
return query.Bool(b => b.MustNot(x => x.Match(qs => qs.Field(propertyExpression).Query((string)value).Operator(Operator.And))));
case "isempty":
return query.QueryString(qs => qs.Fields(fs => fs.Field(propertyExpression)).Query(""));
case "isnotempty":
return query.Bool(b => b.MustNot(x => x.QueryString(qs => qs.Fields(fs => fs.Field(propertyExpression)).Query(""))));
default:
throw new ArgumentOutOfRangeException();
}
}
private System.Linq.Expressions.Expression<Func<T, object>> MapToProperty(string fieldName)
{
switch (fieldName)
{
//return expressions from property names
default:
throw new ArgumentOutOfRangeException();
}
}
private Type MapToType(string fieldName)
{
switch (fieldName)
{
// Map field name to type
// return typeof(String);
default:
throw new ArgumentOutOfRangeException();
}
}
}

You will need to build in your own express and type mapping for properties, but otherwise this is built for Strings and DateTimes. From this base you should be able to implement different types and queries you would need for the Kendo Grid to work with ElasticSearch.

3 thoughts on “Using Angular Kendo Grid with Elastic Search and ASP.NET Core

Leave a Reply