DapperLib / Dapper

Dapper - a simple object mapper for .Net
https://www.learndapper.com/
Other
17.46k stars 3.67k forks source link

How to map a single property Dictionary<Guid,string> with a custom ITypeHandler #225

Open Tasteful opened 9 years ago

Tasteful commented 9 years ago

I have the same issue as the following SO question but the question is missing any answer :(

http://stackoverflow.com/questions/24268378/save-a-dictionary-as-json-in-sql-with-dapper

I'm trying to map a property of type Dictionary<Guid,string> with a ITypeHandler as JSON into a single column in database but will all the time got the following error.

No mapping exists from object type System.Collections.Generic.KeyValuePair`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.Object, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]] to a known managed provider native type.'.

The ITypeHandler that I have tried with are the following.

internal class DictionaryGuidStringTypeHandler : SqlMapper.TypeHandler<Dictionary<Guid, string>>
    {
        public override void SetValue(IDbDataParameter parameter, Dictionary<Guid, string> value)
        {
            if (value == null)
            {
                parameter.Value = DBNull.Value;
            }
            else
            {
                parameter.Value = Converter.ConvertObjectToJson(value);
            }
        }

        public override Dictionary<Guid, string> Parse(object value)
        {
            var strValue = value as string;
            if (string.IsNullOrEmpty(strValue))
            {
                return null;
            }
            return Converter.ConvertJsonToObject<Dictionary<Guid, string>>(strValue);
        }
    }
    internal class KeyValuePairGuidStringTypeHandler : SqlMapper.TypeHandler<KeyValuePair<Guid, string>>
    {
        public override void SetValue(IDbDataParameter parameter, KeyValuePair<Guid, string> value)
        {
            if (ReferenceEquals(value, default(KeyValuePair<Guid, string>)))
            {
                parameter.Value = DBNull.Value;
            }
            else
            {
                parameter.Value = Converter.ConvertObjectToJson(value);
            }
        }

        public override KeyValuePair<Guid, string> Parse(object value)
        {
            var strValue = value as string;
            if (string.IsNullOrEmpty(strValue))
            {
                return default(KeyValuePair<Guid, string>);
            }
            return Converter.ConvertJsonToObject<KeyValuePair<Guid, string>>(strValue);
        }
    }
    internal class NullableKeyValuePairGuidStringTypeHandler : SqlMapper.TypeHandler<KeyValuePair<Guid, string>?>
    {
        public override void SetValue(IDbDataParameter parameter, KeyValuePair<Guid, string>? value)
        {
            if (value == null)
            {
                parameter.Value = DBNull.Value;
            }
            else
            {
                parameter.Value = Converter.ConvertObjectToJson(value);
            }
        }

        public override KeyValuePair<Guid, string>? Parse(object value)
        {
            var strValue = value as string;
            if (string.IsNullOrEmpty(strValue))
            {
                return null;
            }
            return Converter.ConvertJsonToObject<KeyValuePair<Guid, string>>(strValue);
        }
    }

Is it possible to map a single property on the class to a specific type-handler by code or with an attribute? My feeling are that the error originating from the part that creating the data reader to class mapping and if that should be possible to override some of that logic on a easy way I think that will solve my problem.

I can always create a shadow property that will make the conversion inside the class to/from json and expose as a string on the class but I hope I can avoid that.

BR Patric

jamesholwell commented 9 years ago

Not at all sure if this might work, but have you tried installing the type handler to handle IEnumerable<KeyValuePair<Guid, string>> instead of Dictionary<Guid, string>?

On 21 Dec 2014, at 10:59, Patric Forsgard notifications@github.com wrote:

I have the same issue as the following SO question but the question is missing any answer :(

http://stackoverflow.com/questions/24268378/save-a-dictionary-as-json-in-sql-with-dapper

I'm trying to map a property of type Dictionary<Guid,string> with a ITypeHandler as JSON into a single column in database but will all the time got the following error.

No mapping exists from object type System.Collections.Generic.KeyValuePair`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.Object, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]] to a known managed provider native type.'. The ITypeHandler that I have tried with are the following.

internal class DictionaryGuidStringTypeHandler : SqlMapper.TypeHandler<Dictionary<Guid, string>> { public override void SetValue(IDbDataParameter parameter, Dictionary<Guid, string> value) { if (value == null) { parameter.Value = DBNull.Value; } else { parameter.Value = Converter.ConvertObjectToJson(value); } }

    public override Dictionary<Guid, string> Parse(object value)
    {
        var strValue = value as string;
        if (string.IsNullOrEmpty(strValue))
        {
            return null;
        }
        return Converter.ConvertJsonToObject<Dictionary<Guid, string>>(strValue);
    }
}
internal class KeyValuePairGuidStringTypeHandler : SqlMapper.TypeHandler<KeyValuePair<Guid, string>>
{
    public override void SetValue(IDbDataParameter parameter, KeyValuePair<Guid, string> value)
    {
        if (ReferenceEquals(value, default(KeyValuePair<Guid, string>)))
        {
            parameter.Value = DBNull.Value;
        }
        else
        {
            parameter.Value = Converter.ConvertObjectToJson(value);
        }
    }

    public override KeyValuePair<Guid, string> Parse(object value)
    {
        var strValue = value as string;
        if (string.IsNullOrEmpty(strValue))
        {
            return default(KeyValuePair<Guid, string>);
        }
        return Converter.ConvertJsonToObject<KeyValuePair<Guid, string>>(strValue);
    }
}
internal class NullableKeyValuePairGuidStringTypeHandler : SqlMapper.TypeHandler<KeyValuePair<Guid, string>?>
{
    public override void SetValue(IDbDataParameter parameter, KeyValuePair<Guid, string>? value)
    {
        if (value == null)
        {
            parameter.Value = DBNull.Value;
        }
        else
        {
            parameter.Value = Converter.ConvertObjectToJson(value);
        }
    }

    public override KeyValuePair<Guid, string>? Parse(object value)
    {
        var strValue = value as string;
        if (string.IsNullOrEmpty(strValue))
        {
            return null;
        }
        return Converter.ConvertJsonToObject<KeyValuePair<Guid, string>>(strValue);
    }
}

Is it possible to map a single property on the class to a specific type-handler by code or with an attribute? My feeling are that the error originating from the part that creating the data reader to class mapping and if that should be possible to override some of that logic on a easy way I think that will solve my problem.

I can always create a shadow property that will make the conversion inside the class to/from json and expose as a string on the class but I hope I can avoid that.

BR Patric

— Reply to this email directly or view it on GitHub.

Tasteful commented 9 years ago

@jamesholwell No, that was not working either :(

But found out that its working if I add the input as an instance of SqlMapper.ICustomQueryParameter that will make the conversion for the input object and then the output will work with the following typehandler.

internal class DictionaryGuidStringTypeHandler : SqlMapper.TypeHandler<Dictionary<Guid, string>>
{
    public override void SetValue(IDbDataParameter parameter, Dictionary<Guid, string> value)
    {
        throw new NotSupportedException("SetValue method is not used, use the JsonQueryParameter as the input to serialize the object to json.");
        //if (value == null)
        //{
        //  parameter.Value = DBNull.Value;
        //}
        //else
        //{
        //  parameter.Value = Converter.ConvertObjectToJson(value);
        //}
    }

    public override Dictionary<Guid, string> Parse(object value)
    {
        var strValue = value as string;
        if (string.IsNullOrEmpty(strValue))
        {
            return null;
        }
        return Converter.ConvertJsonToObject<Dictionary<Guid, string>>(strValue);
    }
}

The SqlMapper.ICustomQueryParameter is the following.

internal class JsonQueryParameter : SqlMapper.ICustomQueryParameter
{
    private readonly object _value;

    public JsonQueryParameter(object value)
    {
        _value = value;
    }

    public void AddParameter(IDbCommand command, string name)
    {
        var param = command.CreateParameter();
        param.ParameterName = name;

        if (_value == null)
            param.Value = DBNull.Value;
        else
            param.Value = Converter.ConvertObjectToJson(_value);

        command.Parameters.Add(param);
    }
}

If we switch the place of the if-statement: https://github.com/StackExchange/dapper-dot-net/blob/master/Dapper%20NET40/SqlMapper.cs#L888 and https://github.com/StackExchange/dapper-dot-net/blob/master/Dapper%20NET40/SqlMapper.cs#L893 so we first are checking for ITypehandler and then checking if the input parameter is IEnumerable instead of reverse order.

@mgravell Do you remember why you added the typeHandlers-lookup after the IEnumerable check when execute the LookupDbType-method?

jhovgaard commented 9 years ago

Any news on this? I want to do the same thing.