自定义 F# QueryBuilder

Customizing F# QueryBuilder

如果我像在(F# 交互)中那样创建自定义查询生成器

type T1 = T1 of int list
    with
        member this.Content = let (T1 list) = this in list

type QueryBuilder1() = 
    inherit Linq.QueryBuilder()
        member __.Source (source: T1) = base.Source(source.Content)

let qb1 = new QueryBuilder1()

let list = [1;2;3;4;5]
let t1 = T1 list

let q1 = query { for i in list do select i }
let q2 = query { for i in t1.Content do select i }
let q3 = qb1 { for i in t1 do select i } // OK

一切正常。

但是如果我尝试对 SQL 数据库源进行同样的操作

#r @"C:\Root\Project\Ocnarf\packages\SQLProvider.1.1.44\lib\net451\FSharp.Data.SqlProvider.dll"

let [<Literal>] dbVendor = FSharp.Data.Sql.Common.DatabaseProviderTypes.MSSQLSERVER
let [<Literal>] schemaConnString = @"Data Source=..."
type internal Schema = FSharp.Data.Sql.SqlDataProvider<dbVendor, schemaConnString>
type DowntimeEntity = Schema.dataContext.``dbo.DowntimesEntity``
type DowntimeQuery = System.Linq.IQueryable<DowntimeEntity>

type T2 = T2 of DowntimeQuery
    with
        member this.Query = let (T2 q) = this in q

type QueryBuilder2() = 
    inherit Linq.QueryBuilder()
        member __.Source (source: T2) = base.Source(source.Query)

let db = Schema.GetDataContext()
let tables = db.Dbo
let qry = tables.Downtimes
let t2 = T2 qry
let qb2 = new QueryBuilder2()

let q4 = query { for d in qry do select d }
let q5 = query { for d in t2.Query do select d }
let q6 = qb2 { for d in t2 do select d } // exception

然后我得到以下运行时异常

System.NotSupportedException: This is not a valid query expression. The method 'Microsoft.FSharp.Linq.QuerySource2[FSharp.Data.Sql.Common.SqlEntity,System.Linq.IQueryable] Source[IQueryable](T2)' was used in a query but is not recognized by the F#-to-LINQ query translator. Check the specification of permitted queries and consider moving some of the operations out of the query expression at Microsoft.FSharp.Linq.QueryModule.TransInner$cont@1180-3(Boolean check, FSharpExpr immutQuery, Unit unitVar) at Microsoft.FSharp.Linq.QueryModule.TransInner(CanEliminate canElim, Boolean check, FSharpExpr immutQuery) at Microsoft.FSharp.Linq.QueryModule.TransInner(CanEliminate canElim, Boolean check, FSharpExpr immutQuery) at Microsoft.FSharp.Linq.QueryModule.TransInnerApplicative(Boolean check, FSharpExpr source, FSharpVar immutConsumingVar, FSharpExpr immutConsumingExpr) at Microsoft.FSharp.Linq.QueryModule.TransInner(CanEliminate canElim, Boolean check, FSharpExpr immutQuery) at Microsoft.FSharp.Linq.QueryModule.TransInnerAndCommit(CanEliminate canElim, Boolean check, FSharpExpr x) at Microsoft.FSharp.Linq.QueryModule.TransInnerWithFinalConsume(CanEliminate canElim, FSharpExpr immutSource) at Microsoft.FSharp.Linq.QueryModule.EvalNonNestedInner(CanEliminate canElim, FSharpExpr queryProducingSequence) at Microsoft.FSharp.Linq.QueryModule.EvalNonNestedOuter(CanEliminate canElim, FSharpExpr tm) at Microsoft.FSharp.Linq.QueryModule.clo@1727-1.Microsoft-FSharp-Linq-ForwardDeclarations-IQueryMethods-Execute[a,b](FSharpExpr1 q) at .$FSI_0005.main@()

问题

我应该怎么解决这个异常?

问题是 SQL 查询从 F# 引用翻译成 SQL。这是一个非常敏感的操作,它只适用于具有由标准 F# 查询生成器生成的确切格式的查询。在您的情况下,构造的查询调用您的新 Source 方法,并且翻译仅识别基础 class.

Source 方法

没有简单易行的方法来解决这个问题,但您可以采取一些技巧。您可以重新定义使用创建的查询(F# 引用)调用的 Run 方法,并将引用转换为 F# 到 SQL 翻译器可以理解的形式。

在您的情况下,您可以将 <inst>.Source(<arg>)(您的 Source 方法)替换为 <inst>.Source(<arg>.Query),调用基础 class 的 Source 方法。以下代码执行此操作:

open Microsoft.FSharp.Quotations

let rec replace expr = 
  match expr with 
  | Patterns.Call(Some inst, mi, [arg]) when mi.Name = "Source" -> 
      let sourceMethod =
        typeof<Linq.QueryBuilder>.GetMethods() 
        |> Seq.filter (fun mi -> 
             mi.Name = "Source" && 
             mi.GetParameters().[0].ParameterType.Name = "IQueryable`1")
        |> Seq.head
      let sourceMethod = 
        sourceMethod.MakeGenericMethod 
          [| typeof<DowntimeEntity>; typeof<System.Linq.IQueryable> |]
      let queryProp = typeof<T2>.GetProperty("Query")
      Expr.Call(inst, sourceMethod, [ Expr.PropertyGet(arg, queryProp) ])
  | ExprShape.ShapeCombination(o, args) -> 
      ExprShape.RebuildShapeCombination(o, List.map replace args)
  | ExprShape.ShapeLambda(a, b) -> Expr.Lambda(a, replace b)
  | ExprShape.ShapeVar(v) -> Expr.Var(v)

type QueryBuilder2() = 
    inherit Linq.QueryBuilder()
    member __.Source (source: T2) = base.Source(source.Query)
    member __.Run(e:Expr<Linq.QuerySource<'a, System.Linq.IQueryable>>) = 
        let e = Expr.Cast<Linq.QuerySource<'a, System.Linq.IQueryable>>(replace e.Raw)
        base.Run(e) 

我测试过翻译有效,但我没有运行针对真实的SQL数据库。诀窍是 repalce 函数在查询传递给 SQL 翻译器的 F# 引用之前在查询上调用,并且在 replace 中模式匹配的第一种情况下,它执行上面概述的替换。