Is there a better way to get the names of record fields?

(While this question talks about SQL, because that’s what I’m using at the moment, it’s more of a generalised question really.)

I’ve got some code in which I am generating SQL queries which will be passed to a C# library which I have no control over.

That library takes the SQL string that I give it and replaces the parts of the text that start with “@” (without the quotes) with some actual values (in a parameters record) before execution.

I have stripped the main part of the code out to experiment with, as you can see below where I have three versions.
(The code below doesn’t call the library method with the values in ‘parameters’, it just outputs the SQL string.)

fsi.PrintWidth <- 200 

type RenameParameters = { tableName : string ; rowId : int ; newName : string } 

let idColumnName = "id" 
let nameColumnName = "name" 

module VersionOne = 
    // Basic and ample scope for spelling mistakes.
    let rename = 
        let sql = 
            $"UPDATE @tableName 
              SET {nameColumnName} = @newName 
              WHERE {idColumnName} = @rowId" 
        fun (parameters : RenameParameters) ->
            sql 

module VersionTwo = 
    // Better, but string needs to be evaluated every time.
    let rename (parameters : RenameParameters) = 
        let sql = $"UPDATE @{nameof parameters.tableName} 
                    SET {nameColumnName} = @{nameof parameters.newName} 
                    WHERE {idColumnName} = @{nameof parameters.rowId}" 
        sql

module VersionThree = 
    // A bit better, retains closure, but clumsy.
    let rename = 
        let dummy = { RenameParameters.tableName = "dummy" ; rowId = 0 ; newName = "dummy" } 
        let sql = $"UPDATE @{nameof dummy.tableName} 
                    SET {nameColumnName} = @{nameof dummy.newName} 
                    WHERE {idColumnName} = @{nameof dummy.rowId}" 
        fun (parameters : RenameParameters) -> 
            sql 
        
let parameters = { tableName = "table" ; rowId = 1 ; newName = "newName" } 
let v1 = VersionOne.rename parameters 
let v2 = VersionTwo.rename parameters 
let v3 = VersionThree.rename parameters

I will need to do the same sort of thing in lots of functions, in multiple modules, but the records will always only be simple records with no members or anything fancy.

I’ve looked at Fsharp.Reflection, e.g:

let names = FSharpType.GetRecordFields(typeof<RenameParameters>) |> Array.map (fun prop -> prop.Name)

…but that doesn’t seem to be what I’m looking for.

Can I get the field names in a better way, specifically without the ‘dummy’ value and without having to re-evaluate the string each time?

A lot of times folks use Unchecked.defaultof<RenameParameters> for this sort of thing to create dummy without having to define each field, though the value of dummy will be null so if you try to actually access any values in it, the program will crash.

let dummy = Unchecked.defaultof<RenameParameters> 
let sql = $"UPDATE @{nameof dummy.tableName} 
            SET {nameColumnName} = @{nameof dummy.newName} 
            WHERE {idColumnName} = @{nameof dummy.rowId}"

You could also use reflection like

> [for p in typeof<RenameParameters>.GetProperties() -> p.Name];;
val it: string list = ["tableName"; "rowId"; "newName"]

I thought about using Reflection but that gave me the following code:

module VersionFour = 
    let rename = 
        let fieldName = 
            FSharpType.GetRecordFields(typeof<RenameParameters>) 
            |> Array.map (fun prop -> prop.Name) 
        let sql = $"UPDATE @{fieldName[0]} 
                    SET {nameColumnName} = @{fieldName[2]} 
                    WHERE {idColumnName} = @{fieldName[1]}" 
        fun (parameters : RenameParameters) -> 
            sql

…which offers up its own (easy to break something) problems if I add/remove fields or re-order them, so I decided against it.

There might be a ‘fancier’ way of using the Reflection data but I don’t want to make it more complicated considering what I need it to do.

Even though I’d never heard of Unchecked.defaultof<> before, I think I might go with that as it looks pretty clean and less likely to break later without me noticing it easily.

Thanks again for your help.