henkmollema / Dapper-FluentMap

Provides a simple API to fluently map POCO properties to database columns when using Dapper.
MIT License
429 stars 88 forks source link

Nullable decimal issue #70

Closed dmdymov closed 6 years ago

dmdymov commented 6 years ago

Dapper 1.50.5, Dapper.FluentMap 2.0.0-beta1.

using Dapper;
using Dapper.FluentMap;
using Dapper.FluentMap.Mapping;
using Npgsql;
using NpgsqlTypes;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;

namespace NullableDecimalError
{
    class Program
    {
        static void Main(string[] args)
        {
            FluentMapper.Initialize(config =>
            {
                config.AddMap(new EntityDtoMap());
            });
            var connection = new NpgsqlConnection("connectionstring");
            var procedureName = "test.get_numeric";
            var cursorName = "p_out_cursor";
            var p = new PostgreDynamicParameters();
            p.Add(cursorName, cursorName, NpgsqlDbType.Refcursor, ParameterDirection.InputOutput);

            using (connection)
            {
                connection.Open();
                var transaction = connection.BeginTransaction();
                connection.Execute(procedureName, param: p, commandType: CommandType.StoredProcedure);
                var sql = "fetch all in " + cursorName;
                //expection here
                var result = connection.Query<EntityDto>(sql, null, commandType: CommandType.Text).ToList();
                transaction.Commit();
            }
        }
    }

    public class EntityDto
    {
        public decimal? Value { get; set; }
    }

    public class EntityDtoMap : EntityMappingBuilder<EntityDto>
    {
        public EntityDtoMap()
        {
            EntityMapping.IsCaseSensitive = false;
            Map(p => p.Value).ToColumn("operation_id");
        }
    }

    public class PostgreDynamicParameters : SqlMapper.IDynamicParameters
    {
        private readonly List<NpgsqlParameter> postgreParameters = new List<NpgsqlParameter>();

        public void Add(string name, object value, NpgsqlDbType dbType, ParameterDirection direction)
        {
            var p = new NpgsqlParameter();
            p.ParameterName = "@" + name;
            p.NpgsqlDbType = dbType;
            p.Direction = direction;
            if (value == null)
            {
                p.Value = DBNull.Value;
            }
            else
            {
                p.Value = value;
            }
            postgreParameters.Add(p);
        }

        public void AddParameters(IDbCommand command, SqlMapper.Identity identity)
        {
            var postgreCommand = command as NpgsqlCommand;

            if (postgreCommand != null)
            {
                postgreCommand.Parameters.AddRange(postgreParameters.ToArray());
            }
        }

    }
}

DB:

create table test.test_numeric (
                value numeric
); 

insert into test.test_numeric(value)   
values(123.45);

CREATE OR REPLACE FUNCTION test.get_numeric(
                inout p_out_cursor refcursor
)
RETURNS refcursor AS
$body$
declare
                v_value numeric;
BEGIN
                open p_out_cursor for
                select value as operation_id
      from test_nsi.test_numeric;
END;
$body$
LANGUAGE 'plpgsql';

This code throws ArgumentNullException at Query line. If Value is declared decimal it works just fine.

Stack:

at System.Reflection.Emit.DynamicILGenerator.Emit(OpCode opcode, MethodInfo meth)
   at Dapper.SqlMapper.GetTypeDeserializerImpl(Type type, IDataReader reader, Int32 startBound, Int32 length, Boolean returnNullIfFirstMissing)
   at Dapper.SqlMapper.TypeDeserializerCache.GetReader(IDataReader reader, Int32 startBound, Int32 length, Boolean returnNullIfFirstMissing)
   at Dapper.SqlMapper.TypeDeserializerCache.GetReader(Type type, IDataReader reader, Int32 startBound, Int32 length, Boolean returnNullIfFirstMissing)
   at Dapper.SqlMapper.GetTypeDeserializer(Type type, IDataReader reader, Int32 startBound, Int32 length, Boolean returnNullIfFirstMissing)
   at Dapper.SqlMapper.GetDeserializer(Type type, IDataReader reader, Int32 startBound, Int32 length, Boolean returnNullIfFirstMissing)
   at Dapper.SqlMapper.<QueryImpl>d__136`1.MoveNext()
   at System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)
henkmollema commented 6 years ago

What if you rename the Value property to something else? Value might be a reserved keyword in your database.

meghma commented 6 years ago

This is definitely an issue https://stackoverflow.com/questions/53052693/dapper-fluent-mapping-bug-for-nullable-decimal

henkmollema commented 6 years ago

Taking a quick look at the source of Dapper, the failing call to Emit(opcode, meth) is probably this one where the property setter is resolved from the type map (the type map which Dapper.FluentMap provides), not sure what is causing it to return null though.

I'll look further into this soon.

henkmollema commented 6 years ago

@dmdymov this is definitely an issue with a specific Value property name. The Nullable<T> class has a Value property as well and Dapper.FluentMap is searching for properties on base classes and it finds the Value property of Nullable<decimal> and attempts to map to that.

This obviously is a bug and needs fixing. Perhaps you can try and use another property name in the meanwhile?

dmdymov commented 6 years ago

@henkmollema, we'll try another property name. Thank you.