sp_executesql with user defined table type not behaving correctly

sp_executesql is for executing ad-hoc T-SQL. So you should try:

EXECUTE sp_executesql N'exec dbo.Repro @MetricData',N'@MetricData dbo.UDTT READONLY',@MetricData=@p3

I was tempted to delete this question but figured someone else might run into a similar circumstance.

The root cause was my $updateCommand had not set the CommandType. The default value is Text so my stored procedure was being called correctly. My parameters were being created and passed but as noted in the other answers just because the parameters are available does not mean they are being used.

Code in case anyone was interested in my mistake

    $updateConnection = New-Object System.Data.SqlClient.SqlConnection("Data Source=localhost\localsqla;Initial Catalog=baseline;Integrated Security=SSPI;")
    $updateConnection.Open()

    $updateCommand = New-Object System.Data.SqlClient.SqlCommand($updateQuery)
    $updateCommand.Connection = $updateConnection

    # This is what I was missing
    $updateCommand.CommandType = [System.Data.CommandType]::StoredProcedure
    #$updateCommand.CommandType = [System.Data.CommandType]::Text
    #$updateCommand.CommandType = [System.Data.CommandType]::TableDirect

    $DataTransferFormat = $updateCommand.Parameters.AddWithValue("@MetricData", $dataTable)
    $DataTransferFormat.SqlDbType = [System.Data.SqlDbType]::Structured
    $DataTransferFormat.TypeName = $udtt
    $results = $updateCommand.ExecuteNonQuery()

Running with a value of CommandType of Text would require @rob_farley's solution of changing the value of $updateQuery to "EXEC dbo.Repro @MetricData". At that point, the sp_executesql will map values correctly and life is good.

Running with a CommandType of TableDirect is not supported by the SqlClient Data Provider, as the documentation indicates.

Using a CommandType of StoredProcedure translates to a direct invocation of the stored procedure without the sp_executesql wrapper and works great.