-
Notifications
You must be signed in to change notification settings - Fork 71
/
SqlCommandProvider.fs
218 lines (164 loc) · 10.7 KB
/
SqlCommandProvider.fs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
namespace FSharp.Data
open System
open System.Data
open System.IO
open System.Data.SqlClient
open System.Reflection
open System.Collections.Generic
open System.Runtime.CompilerServices
open System.Configuration
open System.Runtime.Caching
open Microsoft.SqlServer.Server
open Microsoft.FSharp.Core.CompilerServices
open Microsoft.FSharp.Quotations
open FSharp.Data.SqlClient
open ProviderImplementation.ProvidedTypes
[<assembly:TypeProviderAssembly()>]
[<assembly:InternalsVisibleTo("SqlClient.Tests")>]
do()
[<TypeProvider>]
type public SqlCommandProvider(config : TypeProviderConfig) as this =
inherit TypeProviderForNamespaces()
let mutable watcher = null : IDisposable
let nameSpace = this.GetType().Namespace
let assembly = Assembly.LoadFrom( config.RuntimeAssembly)
let providerType = ProvidedTypeDefinition(assembly, nameSpace, "SqlCommandProvider", Some typeof<obj>, HideObjectMethods = true)
let cache = new MemoryCache(name = this.GetType().Name)
do
this.Disposing.Add <| fun _ ->
try
if watcher <> null then watcher.Dispose()
cache.Dispose()
with _ -> ()
do
providerType.DefineStaticParameters(
parameters = [
ProvidedStaticParameter("CommandText", typeof<string>)
ProvidedStaticParameter("ConnectionStringOrName", typeof<string>)
ProvidedStaticParameter("ResultType", typeof<ResultType>, ResultType.Records)
ProvidedStaticParameter("SingleRow", typeof<bool>, false)
ProvidedStaticParameter("ConfigFile", typeof<string>, "")
ProvidedStaticParameter("AllParametersOptional", typeof<bool>, false)
ProvidedStaticParameter("ResolutionFolder", typeof<string>, "")
ProvidedStaticParameter("DataDirectory", typeof<string>, "")
],
instantiationFunction = (fun typeName args ->
let value = lazy this.CreateRootType(typeName, unbox args.[0], unbox args.[1], unbox args.[2], unbox args.[3], unbox args.[4], unbox args.[5], unbox args.[6], unbox args.[7])
cache.GetOrAdd(typeName, value)
)
)
providerType.AddXmlDoc """
<summary>Typed representation of a T-SQL statement to execute against a SQL Server database.</summary>
<param name='CommandText'>Transact-SQL statement to execute at the data source.</param>
<param name='ConnectionStringOrName'>String used to open a SQL Server database or the name of the connection string in the configuration file in the form of “name=<connection string name>”.</param>
<param name='ResultType'>A value that defines structure of result: Records, Tuples, DataTable, or SqlDataReader.</param>
<param name='SingleRow'>If set the query is expected to return a single row of the result set. See MSDN documentation for details on CommandBehavior.SingleRow.</param>
<param name='ConfigFile'>The name of the configuration file that’s used for connection strings at DESIGN-TIME. The default value is app.config or web.config.</param>
<param name='AllParametersOptional'>If set all parameters become optional. NULL input values must be handled inside T-SQL.</param>
<param name='ResolutionFolder'>A folder to be used to resolve relative file paths to *.sql script files at compile time. The default value is the folder that contains the project or script.</param>
<param name='DataDirectory'>The name of the data directory that replaces |DataDirectory| in connection strings. The default value is the project or script directory.</param>
"""
this.AddNamespace(nameSpace, [ providerType ])
member internal this.CreateRootType(typeName, sqlStatementOrFile, connectionStringOrName: string, resultType, singleRow, configFile, allParametersOptional, resolutionFolder, dataDirectory) =
if singleRow && not (resultType = ResultType.Records || resultType = ResultType.Tuples)
then
invalidArg "singleRow" "singleRow can be set only for ResultType.Records or ResultType.Tuples."
let invalidator() =
cache.Remove(typeName) |> ignore
this.Invalidate()
let sqlStatement, watcher' =
let sqlScriptResolutionFolder =
if resolutionFolder = ""
then config.ResolutionFolder
elif Path.IsPathRooted (resolutionFolder)
then resolutionFolder
else Path.Combine (config.ResolutionFolder, resolutionFolder)
Configuration.ParseTextAtDesignTime(sqlStatementOrFile, sqlScriptResolutionFolder, invalidator)
watcher' |> Option.iter (fun x -> watcher <- x)
if connectionStringOrName.Trim() = ""
then invalidArg "ConnectionStringOrName" "Value is empty!"
let connectionStringName, isByName = Configuration.ParseConnectionStringName connectionStringOrName
let designTimeConnectionString =
if isByName
then Configuration.ReadConnectionStringFromConfigFileByName(connectionStringName, config.ResolutionFolder, configFile)
else connectionStringOrName
let dataDirectoryFullPath =
if dataDirectory = "" then config.ResolutionFolder
elif Path.IsPathRooted dataDirectory then dataDirectory
else Path.Combine (config.ResolutionFolder, dataDirectory)
AppDomain.CurrentDomain.SetData("DataDirectory", dataDirectoryFullPath)
let conn = new SqlConnection(designTimeConnectionString)
use closeConn = conn.UseLocally()
conn.CheckVersion()
conn.LoadDataTypesMap()
let parameters = DesignTime.ExtractParameters(conn, sqlStatement)
let outputColumns =
if resultType <> ResultType.DataReader
then DesignTime.GetOutputColumns(conn, sqlStatement, parameters, isStoredProcedure = false)
else []
let rank = if singleRow then ResultRank.SingleRow else ResultRank.Sequence
let output = DesignTime.GetOutputTypes(outputColumns, resultType, rank)
let cmdProvidedType = ProvidedTypeDefinition(assembly, nameSpace, typeName, Some typeof<``ISqlCommand Implementation``>, HideObjectMethods = true)
do
cmdProvidedType.AddMember(ProvidedProperty("ConnectionStringOrName", typeof<string>, [], IsStatic = true, GetterCode = fun _ -> <@@ connectionStringOrName @@>))
do //Record
output.ProvidedRowType |> Option.iter cmdProvidedType.AddMember
do //ctors
let sqlParameters = Expr.NewArray( typeof<SqlParameter>, parameters |> List.map QuotationsFactory.ToSqlParam)
let isStoredProcedure = false
let ctorArgsExceptConnection = [
Expr.Value sqlStatement;
Expr.Value isStoredProcedure
sqlParameters;
Expr.Value resultType;
Expr.Value rank
output.RowMapping;
Expr.Value output.ErasedToRowType.PartialAssemblyQualifiedName
]
let ctorImpl = typeof<``ISqlCommand Implementation``>.GetConstructors() |> Seq.exactlyOne
do //default ctor and create factory
let ctor1Params =
[
ProvidedParameter("connectionString", typeof<string>, optionalValue = "")
ProvidedParameter("commandTimeout", typeof<int>, optionalValue = defaultCommandTimeout)
]
let ctor1Body(args: _ list) =
let connArg =
<@@
if not( String.IsNullOrEmpty(%%args.[0])) then Connection.Literal %%args.[0]
elif isByName then Connection.NameInConfig connectionStringName
else Connection.Literal connectionStringOrName
@@>
Expr.NewObject(ctorImpl, connArg :: args.[1] :: ctorArgsExceptConnection)
cmdProvidedType.AddMember <| ProvidedConstructor(ctor1Params, InvokeCode = ctor1Body)
cmdProvidedType.AddMember <| ProvidedMethod("Create", ctor1Params, returnType = cmdProvidedType, IsStaticMethod = true, InvokeCode = ctor1Body)
do //ctor and create factory with explicit connection/transaction support
let ctor2Params =
[
ProvidedParameter("connection", typeof<SqlConnection>)
ProvidedParameter("transaction", typeof<SqlTransaction>, optionalValue = null)
ProvidedParameter("commandTimeout", typeof<int>, optionalValue = defaultCommandTimeout)
]
let ctor2Body (args: _ list) =
Expr.NewObject(ctorImpl, <@@ Connection.``Connection and-or Transaction``(%%args.[0], %%args.[1]) @@> :: args.[2] :: ctorArgsExceptConnection)
let ctor2 =
ProvidedConstructor [
ProvidedParameter("transaction", typeof<SqlTransaction>)
ProvidedParameter("commandTimeout", typeof<int>, optionalValue = defaultCommandTimeout)
]
ctor2.InvokeCode <-
fun args -> Expr.NewObject(ctorImpl, <@@ let tran: SqlTransaction = %%args.[0] in Connection.``Connection and-or Transaction``(tran.Connection, tran) @@> :: args.[1] :: ctorArgsExceptConnection)
cmdProvidedType.AddMember <| ProvidedConstructor(ctor2Params, InvokeCode = ctor2Body)
cmdProvidedType.AddMember <| ProvidedMethod("Create", ctor2Params, returnType = cmdProvidedType, IsStaticMethod = true, InvokeCode = ctor2Body)
do //AsyncExecute, Execute, and ToTraceString
let executeArgs = DesignTime.GetExecuteArgs(cmdProvidedType, parameters, allParametersOptional, udtts = [])
let interfaceType = typedefof<ISqlCommand>
let name = "Execute" + if outputColumns.IsEmpty && resultType <> ResultType.DataReader then "NonQuery" else ""
let addRedirectToISqlCommandMethod outputType name =
DesignTime.AddGeneratedMethod(parameters, executeArgs, allParametersOptional, cmdProvidedType.BaseType, outputType, name)
|> cmdProvidedType.AddMember
addRedirectToISqlCommandMethod output.ProvidedType "Execute"
let asyncReturnType = ProvidedTypeBuilder.MakeGenericType(typedefof<_ Async>, [ output.ProvidedType ])
addRedirectToISqlCommandMethod asyncReturnType "AsyncExecute"
addRedirectToISqlCommandMethod typeof<string> "ToTraceString"
cmdProvidedType