Skip to content

Instantly share code, notes, and snippets.

@Luiz-Monad
Last active July 21, 2020 13:15
Show Gist options
  • Save Luiz-Monad/c10851b84a3c21c755e3a6603aeeb7a8 to your computer and use it in GitHub Desktop.
Save Luiz-Monad/c10851b84a3c21c755e3a6603aeeb7a8 to your computer and use it in GitHub Desktop.
graphql-less query creator
namespace Controller.Selector
open System
open log4net
open System.Linq
open System.Web.Http
open Microsoft.FSharp.Linq
open Microsoft.FSharp.Quotations
open Backend.Helpers
open Backend.TypeHelpers
open Backend.Model.Selection
open Backend.ExpressionHelpers
open Backend.Extension.DbContextModule
module SelectorModule =
exception InvalidColumnExn of string
let DataIdColumn = "Id"
//Generic column.
[<AbstractClass>]
type Column<'T> () =
abstract member map : 'T Linq.IQueryable Expr -> 'T Linq.IQueryable Expr
static member create ( templateType : Type ) column ( constructorParam : obj ) =
let p = typeof<'T>.GetProperty column
InvalidColumnExn column |> raiseWhen ( isNull p )
let gt = templateType.GetGenericTypeDefinition ()
let g = gt.MakeGenericType ( typeof<'T>, p.PropertyType )
Activator.CreateInstance ( g, constructorParam ) :?> Column<'T>
//Filter typed for the column.
type Filter<'T, 'K when 'K : comparison> ( param ) =
inherit Column<'T> ()
override this.map source =
let ( column, oValue : obj, operator ) = param
let value = prepareValue oValue
let finalPredicate =
let col = quotedProperty<'T, 'K> column
let like = <@ fun x y -> ( x :> obj :?> string ).Contains( y :> obj :?> string ) @>
match operator with
| LessThan -> <@ fun record -> (%col) record < value @>
| LessOrEqualThan -> <@ fun record -> (%col) record <= value @>
| Equal -> <@ fun record -> (%col) record = value @>
| GreaterThan -> <@ fun record -> (%col) record > value @>
| GreaterOrEqualThan -> <@ fun record -> (%col) record >= value @>
| Like -> <@ fun record -> (%like) ( (%col) record ) value @>
<@ query {
for entity in (%source) do
where ( (%finalPredicate) entity )
select entity } @>
//Process filtering.
let processFiltering selector source =
match selector with
| { Filters = filters } when isNotNull filters ->
let col source ( column, value, operator ) =
let p = box ( column, value, operator )
let fn = Column<'T>.create typeof<Filter<_, _>> column p
fn.map source
filters |> List.fold col source
| _ -> source
//Filter typed for the column.
type Sorter<'T, 'K when 'K : comparison> ( param ) =
inherit Column<'T> ()
override this.map source =
let ( column, order, first ) = param
let col = quotedProperty<'T, 'K> column
match ( order, first ) with
| Ascending, true -> <@ query {
for item in (%source) do
sortBy ( (%col) item )
thenBy ( (%col) item ) } @>
| Descending, true -> <@ query {
for item in (%source) do
sortByDescending ( (%col) item ) } @>
| Ascending, false -> <@ query {
for item in (%source) do
thenBy ( (%col) item ) } @>
| Descending, false -> <@ query {
for item in (%source) do
thenByDescending ( (%col) item ) } @>
//Process sorting.
let processSorting selector source =
let col source ( column, order, first ) =
let p = box ( column, order, first )
let fn = Column<'T>.create typeof<Sorter<_, _>> column p
fn.map source
match selector with
| { Columns = columns } when isNotNull columns && columns.Length > 0 ->
let item ( ix, ( col, order ) ) = ( col, order, ix = 0 )
columns |> List.indexed |> List.map item |> List.fold col source
| _ ->
col source ( DataIdColumn, Descending, true )
//Process paginating.
let processPaging selector source =
match selector with
| pager when isNotNull pager ->
let ct = pager.RecordCount |> max 10
let id = pager.PageNumber |> max 1
let takeCount = ct
let skipCount = (id - 1) * ct
let createQuery src =
<@ query {
for entity in (%src) do
select entity
skip skipCount
take takeCount } @>
source |> createQuery
| _ -> source
//Process key search.
let processKey id source =
processFiltering
<| { Filters = [ ( DataIdColumn, box id, Equal ) ] }
<| source
//Process paginating.
let processTotal source =
<@ query {
for entity in (%source) do
count } @>
//Process selector modes.
let processTotalSelector selector =
processFiltering selector >>
processTotal >>
runQueryValue
//Process selector modes.
let processSelector selector =
let noLazy = Seq.toList >> Seq.ofList
match selector with
| AllData ->
runQuery >> noLazy
| Key k ->
processKey k >>
runQuery >> noLazy
| Selector s ->
processFiltering s.Filter >>
processSorting s.Sorter >>
processPaging s.Paginator >>
runQuery >> noLazy
| Total _ ->
fun _ -> Seq.empty
[<AutoOpen>]
module SelectorControllerModule =
open SelectorModule
//Add the trait to the controller.
let selectorController dataAccess controller =
let read = function
| Some ( Total t ) -> dataAccess () |> processTotalSelector t |> Totals
| Some s -> dataAccess () |> processSelector s |> Records
| None -> Seq.empty |> Records
controller read
//Deep clone the selector, used for type punning.
let inline selectorCopy o =
o |> whenSome selectorDeepCopy
// copyright Luiz Felipe Eugenio Stangarlin, circa 2015
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment