Dapper icon indicating copy to clipboard operation
Dapper copied to clipboard

Type handler with generic type

Open regazzoj opened this issue 2 years ago • 6 comments

Hello, For now, we can add type handler for class without generic type.

In our software, we need possibility to add a type handler for a generic type like this one => public class Ref<T> {...}

We think that there could be a delegate function which check if the given object can be assignable to the generic type when we add a type handler => SqlMapper.AddTypeHandler(t => t.IsAssignableTo(typeof(Ref)), new RefTypeHandler());

And to finish, the type handler methods should have the wanted type in parameter, so we can parse and set correctly =>

public sealed class RefTypeHandler : SqlMapper.TypeHandler<Ref>
{
    public override void SetValue(IDbDataParameter parameter, Ref value, Type type)
    {
        throw new System.NotImplementedException();
    }

    public override Ref Parse(object value, Type type)
    {
        throw new System.NotImplementedException();
    }
}

This is not an issue, but a new functionality, but I don't know where to write it, and it might be not relevant as well. Just let me know !

Regards !

regazzoj avatar Jun 20 '23 08:06 regazzoj

Yes, the type-handler API is limited and applied inconsistently; if we make a change here, it will be over in the AOT project, but what I'm thinking is something along the lines of:

/// <summary>
/// Specify that <typeparamref name="TTypeHandler"/> should be used as a custom type-handler
/// when processing values of type <typeparamref name="TValue"/>
/// </summary>
[ImmutableObject(true)]
[AttributeUsage(AttributeTargets.All, AllowMultiple = true)]
public sealed class TypeHandlerAttribute<TValue, TTypeHandler> : Attribute
    where TTypeHandler : TypeHandler<TValue>, new()
{}

/// <summary>
/// Process a parameter value of type <typeparamref name="T"/>
/// </summary>
public abstract class TypeHandler<T>
{
    /// <summary>
    /// Configure and assign a value for a parameter
    /// </summary>
    public virtual void SetValue(DbParameter parameter, T value)
        => parameter.Value = CommandFactory.AsGenericValue(value); // default handling, already exists

    /// <summary>
    /// Interpret the output value from a parameter
    /// </summary>
    public virtual T Parse(DbParameter parameter)
        => CommandUtils.As<T>(parameter.Value); // default handling, already exists
}

with example usage:

[module: TypeHandler<SomeType, SomeTypeHandler>]
[module: TypeHandler<SomeRef<object>, SomeRefHandler<object>>] // object here is placeholder only

class SomeTypeHandler : TypeHandler<SomeType> { }
struct SomeType { }

class SomeRef<T> { }
class SomeRefHandler<T> : TypeHandler<SomeRef<T>> { }

which would allow SomeRefHandler<T> to override Parse and/or SetValue, and do whatever it needs; this would be after the default config (DbType, Size, etc), and would be instead of the default .Value = ...

Would this meet your idea?

mgravell avatar Jun 20 '23 09:06 mgravell

Hello Marc,

thanks for your quick answer. I'm working with @regazzoj, so I'm aware of what he ask.

We are not sure to understand your answer (a little confusing for us).

I will try to explain you the problem we are facing with the actual version of DapperLib :

As mentioned in the issue earlier we have a generic Ref<T>. This type inherits from Ref abstract class.

public abstract class Ref{}

public class Ref<T> : Ref {}

Today with Dapper (not AOT) we can easily define a TypeHandler like this one :

// Version 1
public class RefTypeHandler<T> : SqlMapper.TypeHandler<Ref<T>>
{
    public override void SetValue(IDbDataParameter parameter, Ref<T> value)
    {
        throw new System.NotImplementedException();
    }

    public override Ref<T> Parse(object value)
    {
        throw new System.NotImplementedException();
    }
}

or this one

// Version 2
public class RefTypeHandler : SqlMapper.TypeHandler<Ref>
{
    public override void SetValue(IDbDataParameter parameter, Ref value)
    {
        throw new System.NotImplementedException();
    }

    public override Ref Parse(object value)
    {
        throw new System.NotImplementedException();
    }
}

The problem is in the 2 cases we cannot register the typehandler in Dapper to handler all generic Ref<T> types.

// Version 1
SqlMapper.AddTypeHandler(new RefTypeHandler());
// only registered for Ref abstract type, does not match any Ref<T> subtypes at runtime.
// Version 2
SqlMapper.AddTypeHandler(typeof(Ref<>), typeof(RefTypeHandler<>));
// does not compile

The only solution is to manually register each concrete type as this :

// Version 3
SqlMapper.AddTypeHandler(new RefTypeHandler<Referential>()); // register Ref<Referential>
SqlMapper.AddTypeHandler(new RefTypeHandler<UserGroup>()); // register Ref<UserGroup>
// ... but the list can grow

maybe the problem is clearer for you now ?

agjini avatar Jun 21 '23 08:06 agjini

It was never not clear. My point is: yes, we know it is limited right now, and my plan is to focus all feature additions at the AOT model, which is many times easier (and more efficient) to maintain, so I was trying to be clear "if we make a change to this, it is likely to be in the AOT timescale and require the AOT lib"

mgravell avatar Jun 22 '23 05:06 mgravell

Great ! Thanks for your answer ! The syntax used in your previous answer was confusing for us. Too much things we don't know about... Thanks for your time ! Can't wait to see AOT lib then :smile:

regazzoj avatar Jun 22 '23 09:06 regazzoj

Why not just use [TypeConverter] attributes that may be on the target type? You probably have common mappings for types already, for example you can already map a SqlString to a string, so if the type has a [TypeConverter(typeof(ExampleConverter))] which points to a converter type of ValueConverter<Example, string>.

Then you'd use the converter to convert to and from a string and your built-in mapper to map to known types. Is that not reasonable? Many types are already have these converters.

justinmchase avatar Oct 03 '23 13:10 justinmchase

Why not just use [TypeConverter] attributes that may be on the target type? You probably have common mappings for types already, for example you can already map a SqlString to a string, so if the type has a [TypeConverter(typeof(ExampleConverter))] which points to a converter type of ValueConverter<Example, string>.

Then you'd use the converter to convert to and from a string and your built-in mapper to map to known types. Is that not reasonable? Many types are already have these converters.

I have a scenario where I need to type convert date times to UTC (by specifying the kind from Unknown to UTC) but I dont want to map all DateTimes to UTC. Right now, its all or nothing and I have no way of saying only apply the mapping to this property. Attributes are really needed for scenarios like these.

KieranDevvs avatar Jun 05 '24 14:06 KieranDevvs