SqlNullValueException when executing a stored procedure with MySqlCommand

3

I'm writing a C# application to retrieve recipes from a MySQL database, using Dapper for ORM. So far, I've written my DAL in C# with direct queries (Which I know is unsafe), and this works great. I've now started to transition over to stored procedures with parameters to better protect the database from SQL injection, as well as just using as close to best practice as I can.

However, when I'm using Dapper's QueryAsync<T> (This also applies to Query<T>) along with DynamicParameters, I get an exception, with the message "Data is Null. This method or property cannot be called on Null values."

However, if I either execute the query as a string literal SQL statement, or use a string literal to call the stored procedure, it works fine. I know the data is there, and not null, because it works when running it directly in MySQL with a set id number I know exists. I have also tried running the methods listed below in C# with an id I know exists, some of them work fine, some of them return the stated error.

I have no idea where this is failing once I make the QueryAsync<Recipe>("...") call. I don't know if the parameters I provide to the method are not being passed into the stored procedure, or if the procedure is returning null, or something else if going wrong.

Any help with working out where this may be failing with that call would be greatly appreciated. I've included the stack trace at the bottom, which I can't make sense of as yet. I still need to learn to understand stack traces.

Edit: I have recreated the MySql database in SQL Server, and created a new DAL connector. All exactly mirroring the MySql structure and DAL. GetRecipeByIdAsync1(int id) works exactly as expected with SQL Server. So there must be something about the way Dapper/DynamicParameters/MySql.Data is interacting with the stored procedure in MySQL

My Recipe class:

public class Recipe
{

        [Description("id")]
        public int Id { get; set; }

        [Description("name")]
        public string Title { get; set; }

        [Description("description")]
        public string Description { get; set; }

        [Description("source_site")]
        public string SourceSite { get; set; }
}

This is my recipes table in MySQL:

recipes
=============
id (pk)     | INT          | Not Null   | Auto-Increment
name        | VARCHAR(45)  | Not Null   |
description | VARCHAR(250) | Allow Null |
source_site | VARCAHR(200) | Allow Null |

This is the helper class I'm using to set the custom mapping so my columns don't need to match the property names:

public class Helper
{
    public static void SetTypeMaps()
    {
        var recipeMap = new CustomPropertyTypeMap(typeof(Recipe),
            (type, columnName) => type.GetProperties().FirstOrDefault(prop => GetDescriptionFromAttribute(prop) == columnName));

        SqlMapper.SetTypeMap(typeof(Recipe), recipeMap);

        // Other custom mappers omitted
    }

The stored procedure I'm using:

PROCEDURE `sp_recipes_GetByRecipeId`(IN RecipeId INT)
BEGIN
    SELECT r.*
    FROM recipes r
    WHERE r.id = RecipeId;
END

Now for the various versions of the method I'm using in my DAL (I've numbered them here for ease):

/// This does not work
public async Task<Recipe> GetRecipeByIdAsync1(int id)
{
    using (IDbConnection db = new MySqlConnection(GlobalConfig.CnnString("CookbookTest1")))
    {
        var p = new DynamicParameters();
        p.Add("RecipeId", id, dbType: DbType.Int32, direction: ParameterDirection.Input);

        // This is the line where the exception occurs
        var result = await db.QueryAsync<Recipe>("sp_recipes_GetByRecipeId", p, commandType: CommandType.StoredProcedure); 

        return result.FirstOrDefault();
    }

}

// This also does not work
public async Task<Recipe> GetRecipeByIdAsync2(int id)
{
    using (IDbConnection db = new MySqlConnection(GlobalConfig.CnnString("CookbookTest1")))
    {
        // This is the line where the exception occurs
        var result = await db.QueryAsync<Recipe>("sp_recipes_GetByRecipeId", new {RecipeID = id}, commandType: CommandType.StoredProcedure); 

        return result.FirstOrDefault();
    }

}

// Nor this
public async Task<Recipe> GetRecipeByIdAsync3(int id)
{
    using (IDbConnection db = new MySqlConnection(GlobalConfig.CnnString("CookbookTest1")))
    {
        // This is the line where the exception occurs
        var result = await db.QueryAsync<Recipe>("sp_recipes_GetByRecipeId", new {id}, commandType: CommandType.StoredProcedure); 

        return result.FirstOrDefault();
    }

}

// This works perfectly, but I'm not sure how safe it is
public async Task<Recipe> GetRecipeByIdAsync4(int id)
{
    using (IDbConnection db = new MySqlConnection(GlobalConfig.CnnString("CookbookTest1")))
    {
        var result = await db.QueryAsync<Recipe>($"call sp_recipes_GetByRecipeId({id})"); 

        return result.FirstOrDefault();
    }

}

// And of course, this works, but is horrible practice
public async Task<Recipe> GetRecipeByIdAsync5(int id)
{
    using (IDbConnection db = new MySqlConnection(GlobalConfig.CnnString("CookbookTest1")))
    {
        var result = await db.QueryAsync<Recipe>($"SELECT * FROM recipes WHERE recipes.id = {id}"); 

        return result.FirstOrDefault();
    }

}

Connection string if anyone wanted

<connectionStrings>
    <add name="CookbookTest1" connectionString="Server=localhost;Database=cookbook_test1;Uid=vs_dev;Pwd=developer;" providerName="MySql.Data"/>
</connectionStrings>

Stack trace:

System.Data.SqlTypes.SqlNullValueException
  HResult=0x80131931
  Message=Data is Null. This method or property cannot be called on Null values.
  Source=MySql.Data
  StackTrace:
   at MySql.Data.MySqlClient.MySqlDataReader.GetFieldValue(Int32 index, Boolean checkNull)
   at MySql.Data.MySqlClient.MySqlDataReader.GetString(Int32 i)
   at MySql.Data.MySqlClient.MySqlDataReader.GetString(String column)
   at MySql.Data.MySqlClient.SchemaProvider.GetProcedures(String[] restrictions)
   at MySql.Data.MySqlClient.ISSchemaProvider.GetProcedures(String[] restrictions)
   at MySql.Data.MySqlClient.ISSchemaProvider.GetSchemaInternal(String collection, String[] restrictions)
   at MySql.Data.MySqlClient.SchemaProvider.GetSchema(String collection, String[] restrictions)
   at MySql.Data.MySqlClient.MySqlConnection.GetSchemaCollection(String collectionName, String[] restrictionValues)
   at MySql.Data.MySqlClient.ProcedureCache.GetProcData(MySqlConnection connection, String spName)
   at MySql.Data.MySqlClient.ProcedureCache.AddNew(MySqlConnection connection, String spName)
   at MySql.Data.MySqlClient.ProcedureCache.GetProcedure(MySqlConnection conn, String spName, String cacheKey)
   at MySql.Data.MySqlClient.StoredProcedure.GetParameters(String procName)
   at MySql.Data.MySqlClient.StoredProcedure.CheckParameters(String spName)
   at MySql.Data.MySqlClient.StoredProcedure.Resolve(Boolean preparing)
   at MySql.Data.MySqlClient.MySqlCommand.ExecuteReader(CommandBehavior behavior)
   at MySql.Data.MySqlClient.MySqlCommand.ExecuteDbDataReader(CommandBehavior behavior)
   at System.Data.Common.DbCommand.ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd(Task task)
   at Dapper.SqlMapper.<QueryAsync>d__33`1.MoveNext() in C:\projects\dapper\Dapper\SqlMapper.Async.cs:line 468
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at CookbookLibrary.DataAccess.MySqlConnector.<TestStoredProcAsync>d__5.MoveNext() in C:\Users\cyclone\Desktop\VS Projects\DigitalCookbook\CookbookLibrary\DataAccess\MySqlConnector.cs:line 119
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at DigitalCookbook.ViewModel.MainWindowModel.<TestProcedure>d__38.MoveNext() in C:\Users\cyclone\Desktop\VS Projects\DigitalCookbook\DigitalCookbook\ViewModel\MainWindowModel.cs:line 228
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
   at DigitalCookbook.ViewModel.MainWindowModel.<<get_TestCommand>b__31_0>d.MoveNext() in C:\Users\cyclone\Desktop\VS Projects\DigitalCookbook\DigitalCookbook\ViewModel\MainWindowModel.cs:line 114

c#
mysql
stored-procedures
orm
dapper
asked on Stack Overflow Dec 9, 2019 by MatazaNz • edited Mar 13, 2020 by Bradley Grainger

1 Answer

2

This looks like a bug in Oracle's MySQL Connector/NET (aka MySql.Data). It doesn't look like any bug that I'm familiar with in that bug database; it might need to be filed as a new issue. (Bug 75301 looks similar but it's not immediately obvious that it's the same issue.)

I would recommend switching to MySqlConnector; it's an alternate ADO.NET library for MySQL that has great compatibility with Dapper and fixes many known bugs in MySQL Connector/NET. MySqlConnector also has true async I/O support, which is not implemented in Connector/NET; this will be important if you want to use QueryAsync in your code.

If you want to keep using Oracle's MySQL Connector/NET, you may be able to work around the problem by adding CheckParameters=false to your connection string. Note that this could be a breaking change to your code; if you set the setting to false, you'll have to manually ensure that the parameters added to each CommandType.StoredProcedure MySqlCommand are in the exact same order as the database (because MySql.Data will no longer fix them up for you).

Update: After looking at the Connector/NET source code, it appears that your database has some data it's not expecting. Does either of the following two queries produce rows? If so, which value(s) are NULL?

SELECT * FROM information_schema.routines
WHERE specific_name IS NULL OR
    routine_schema IS NULL OR
    routine_name IS NULL OR
    routine_type IS NULL OR
    routine_definition IS NULL OR
    is_deterministic IS NULL OR
    sql_data_access IS NULL OR
    security_type IS NULL OR
    sql_mode IS NULL OR
    routine_comment IS NULL OR
    definer IS NULL;

SELECT * FROM mysql.proc
WHERE specific_name IS NULL OR
    db IS NULL OR
    name IS NULL OR
    type IS NULL OR
    body IS NULL OR
    is_deterministic IS NULL OR
    sql_data_access IS NULL OR
    security_type IS NULL OR
    sql_mode IS NULL OR
    comment IS NULL OR
    definer IS NULL;

What MySQL Server are you using (MySQL, MariaDB, Amazon Aurora) and which version?

answered on Stack Overflow Dec 9, 2019 by Bradley Grainger • edited Mar 13, 2020 by Bradley Grainger

User contributions licensed under CC BY-SA 3.0