Search code examples
c#sql-serverdapperjil

How to use Dapper's multi-mapping while mapping one type to another?


My scenario: I have a class called Person that I save to the DB using Dapper. On Person, I have a Dictionary of Values that I serialize to a string and store that as varchar(MAX).

public class Person
{
    public string Name {get; set;}
    public int Id {get; set;} //PK in DB
    public int Age {get; set;}
    public IDictionary<string, string> Values {get; set;}
}

This is how I save to the DB:

DynamicProperties dp = new DynamicProperties();
dp.Add("Name", p.Name);
dp.Add("Age", p.Age);
dp.Add("Values",  Jil.JSON.Serialize<IDictionary<string, string>>(p.Values));

conneciton.Execute(insertSql, dp, commandType: CommandType.StoredProcedure);

This is where I try to read it out

    private Func<Person, object, Person> GetPerson = new Func<Person, object, Person>
((person, values) => {
                person.Values = Jil.JSON.Deserialize<Dictionary<string, string>>((string)values);
                return person;
            });

string sql =   "SELECT text
                FROM otherTable

                SELECT Name, Id, Age, Values 
                FROM People 
                WHERE Id = @Id"

SqlMapper.GridReader gridReader = connToDeviceConfig.QueryMultiple(sql, new {Id = 5}, commandType: CommandType.StoredProcedure);

List<string> listOfOtherStuff = gridReader.Read<string>().ToList();
List<Person> people = gridReader.Read<Person, object, Person>(GetPerson, splitOn: "Age").ToList();

// listOfOtherStuff and people are seperate

The second gridReader fails with When using the multi-mapping APIs ensure you set the splitOn param if you have keys other than Id\r\nParameter name: splitOn

I feel like I am perhaps bending Dapper a bit to try and make it do something it wasn't supposed to do, i.e. reading a string out of the DB and deserializing it into a Dictionary and assigning to Person.Values.

Is this the way to do it (and I just have a bug somewhere)? or is there another approach that I should take?

I used this as a ref: (Link to approximate location in file at Archive.org)

public void TestProcSupport()
{
    var p = new DynamicParameters();
    p.Add("a", 11);
    p.Add("b", dbType: DbType.Int32, direction: ParameterDirection.Output);
    p.Add("c", dbType: DbType.Int32, direction: ParameterDirection.ReturnValue);

    connection.Execute(@"create proc #TestProc 
@a int,
@b int output
as 
begin
set @b = 999
select 1111
return @a
end");
    connection.Query<int>("#TestProc", p, commandType: CommandType.StoredProcedure).First().IsEqualTo(1111);

    p.Get<int>("c").IsEqualTo(11);
    p.Get<int>("b").IsEqualTo(999);

}

StackTrace:

   at Dapper.SqlMapper.GetNextSplit(Int32 startIdx, String splitOn, IDataReader reader) in D:\Dev\dapper-dot-net\Dapper NET40\SqlMapper.cs:line 2111
   at Dapper.SqlMapper.GenerateDeserializers(Type[] types, String splitOn, IDataReader reader) in D:\Dev\dapper-dot-net\Dapper NET40\SqlMapper.cs:line 2057
   at Dapper.SqlMapper.<MultiMapImpl>d__71`8.MoveNext() in D:\Dev\dapper-dot-net\Dapper NET40\SqlMapper.cs:line 1857
   at Dapper.SqlMapper.GridReader.<MultiReadInternal>d__9`8.MoveNext() in D:\Dev\dapper-dot-net\Dapper NET40\SqlMapper.cs:line 4300
   at System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)
   at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
   at Dapper.SqlMapper.GridReader.Read[TFirst,TSecond,TReturn](Func`3 func, String splitOn, Boolean buffered) in D:\Dev\dapper-dot-net\Dapper NET40\SqlMapper.cs:line 4330

Solution

  • Tried other splitOns?

    I don't have a DB test with handy, but I think splitOn is supposed to be the first column you want in the next mapped object, not the last column in the previous. Also the columns become members of an object, not the whole value (though there may be an exception for single-column cases, again I don't have a test DB handy).

    TL;DR - try something like this

    class ValuesObj{
        public string Values {get;set;}
    }
    
    // whatever code here
    
    private Func<Position, ValuesObj, Position> GetPerson = new Func<Person, ValuesObj, Person>
    ((person, valuesObj) => {
                    string values = valuesObj.Values;
                    person.Values = Jil.JSON.Deserialize<Dictionary<string, string>>((string)values);
                    return position;
                });
    
    string sql =   "SELECT text
                    FROM otherTable
    
                    SELECT Name, Id, Age, Values 
                    FROM People 
                    WHERE Id = @Id"
    
    SqlMapper.GridReader gridReader = connToDeviceConfig.QueryMultiple(sql, new {Id = 5}, commandType: CommandType.StoredProcedure);
    
    List<string> listOfOtherStuff = gridReader.Read<string>().ToList();
    List<Person> people = gridReader.Read<Person, ValuesObj, Person>(GetPerson, splitOn: "Values").ToList();
    

    Otherwise, I'd suggest just using the dynamic return Dapper methods.