Skip to content

InvokeAsync on a stored procedure does not catch exceptions for procs without return data #667

Closed
@replicaJunction

Description

@replicaJunction

Description

Some stored procedures return result sets of data, but others just return int status codes to indicate success or failure. SQLProvider appears to ignore those status codes, and calling Invoke or InvokeAsync on one of those procedures returns Unit or Async<Unit> respectively.

When one of these procedures throws an error, Invoke raises a SqlException with the text of the error thrown from the procedure. This can be caught using a normal try...with block. However, the InvokeAsync method does not appear to catch this error, and the code continues as if the procedure was successful.

Also mentioned in the last comment in #535.

Repro steps

Use this SQL to create a table, procedure, and some example rows:

-- Create example table
DROP TABLE IF EXISTS [dbo].[TempTesting]
GO

CREATE TABLE [dbo].[TempTesting](
	[Id] [int] IDENTITY(1,1) NOT NULL,
	[Name] [varchar](50) NOT NULL,
    PRIMARY KEY CLUSTERED ([Id] ASC)
);
GO

-- Create example stored procedure
CREATE OR ALTER PROCEDURE [dbo].[spDoTempTesting]
    @Name VARCHAR (50)
AS
BEGIN
    DECLARE
         @existingId INT
        ,@errorMsg VARCHAR(50)
        ;

    SELECT @existingId = [Id] FROM [dbo].[TempTesting] WHERE Name = @Name;

    IF @existingId IS NULL
    BEGIN
        SET @errorMsg = CONCAT(
            'Could not find name [',
            @Name,
            ']'
        );
        THROW 50001, @errorMsg, 1
    END

    --SELECT * FROM [dbo].[TempTesting] WHERE Id = @existingId;
    RETURN 0
END
GO

-- Create example data
INSERT INTO [dbo].[TempTesting] ([Name]) VALUES ('John'), ('Sarah'), ('George')
GO

Now, run this F# code:

// #r statements excluded for brevity
// Also, assume the variable connectionString here represents a valid connection string.

open FSharp.Data.Sql

type sql = SqlDataProvider<
                ConnectionString = connectionString,
                DatabaseVendor = Common.DatabaseProviderTypes.MSSQLSERVER
                >


let ctx = sql.GetDataContext(SelectOperations.DatabaseSide)

let f name =
    try
        let normalResult = ctx.Procedures.SpDoTempTesting.Invoke(``@Name``=name)
        printfn "Normal result succeeded:\n%A\n\n" normalResult
    with e ->
        eprintfn "Normal result failed:\n%A\n\n" e

    let asyncResult =
        ctx.Procedures.SpDoTempTesting.InvokeAsync(``@Name``=name)
        |> Async.Catch
        |> Async.RunSynchronously

    match asyncResult with
        | Choice1Of2 c -> printfn "Async result succeeded:\n%A\n\n" c
        | Choice2Of2 c -> eprintfn "Async result failed:\n%A\n\n" c


// Should not error, since we created a John record
f "John"

// Should error, since we did not create this one
f "NotARealName"

These are the results of running the above code in F# interactive:

Normal result succeeded:
<null>


Async result succeeded:
<null>


Async result succeeded:
<null>


Normal result failed:
System.Data.SqlClient.SqlException (0x80131904): Could not find name [NotARealName]
   ...stack trace...

Finally, for comparison, modify and run the SQL by uncommenting the last SELECT line and commenting the RETURN line at the end. Repeat the F# code and compare the results.

Normal result succeeded:
FSharp.Data.Sql.Common.SqlEntity


Async result succeeded:
FSharp.Data.Sql.Common.SqlEntity


Normal result failed:
System.Data.SqlClient.SqlException (0x80131904): Could not find name [NotARealName]
    ...stack trace...

Async result failed:
System.AggregateException: One or more errors occurred. ---> System.Data.SqlClient.SqlException: Could not find name [NotARealName]
   at System.Data.SqlClient.SqlCommand.<>c.<ExecuteDbDataReaderAsync>b__180_0(Task`1 result)
   at System.Threading.Tasks.ContinuationResultTaskFromResultTask`2.InnerInvoke
   at System.Threading.Tasks.Task.Execute()
   --- End of inner exception stack trace ---
---> (Inner Exception #0) System.Data.SqlClient.SqlException (0x80131904): Could not find name [NotARealName]
   at System.Data.SqlClient.SqlCommand.<>c.<ExecuteDbDataReaderAsync>b__180_0(Task`1 result)
   at System.Threading.Tasks.ContinuationResultTaskFromResultTask`2.InnerInvoke
   at System.Threading.Tasks.Task.Execute()
ClientConnectionId:ab1983fc-944c-4afa-af60-d28a758a9af8
Error Number:50001,State:1,Class:16<---

Expected behavior

InvokeAsync should return Choice2Of2 with an exception in both test cases.

Actual behavior

In the first case, InvokeAsync appears to succeed, while Invoke returns a clear failure message. In the second test case, both Invoke and InvokeAsync failed as expected.

Known workarounds

I can think of two workarounds:

  1. Modify all stored procedures to return data, not just result codes
  2. Modify all F# code calling stored procedures to be synchronous

Neither of these are great options.

Related information

  • Used database: Microsoft SQL Server 14.0.2027
  • Operating system: Windows 10
  • Branch: NuGet release, version 1.1.76
  • .NET Runtime, CoreCLR or Mono Version: .NET Core 3.1
  • Performance information, links to performance testing scripts: N/A

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions