Vogen icon indicating copy to clipboard operation
Vogen copied to clipboard

Generate dapper type handlers outside the domain project

Open marcwittke opened this issue 11 months ago • 2 comments

Describe the feature

Referring to the Note in the docs I'd like to request dapper type handlers to be generated outside the project. It was mentioned in #541 but it seems as it wasn't implemented for Dapper.

marcwittke avatar Jan 03 '25 21:01 marcwittke

I found a workaround and I wonder if this could be a first class citizen in Vogen (or in a separate extension project Vogen.Dapper?) Using a little bit of reflection I have a generic type handler:

using System.Data;
using System.Reflection;
using Dapper;


public class StrongIdValueTypeHandler<TValueObject> : SqlMapper.TypeHandler<TValueObject> where TValueObject : struct, IEquatable<int>
{
    // ReSharper disable once StaticMemberInGenericType
    private static readonly MethodInfo FromMethod;

    // ReSharper disable once StaticMemberInGenericType
    private static readonly PropertyInfo ValueProperty;

    static StrongIdValueTypeHandler()
    {
        FromMethod = typeof(TValueObject).GetMethod("From", BindingFlags.Public | BindingFlags.Static)
            ?? throw new InvalidOperationException($"The type {typeof(TValueObject)} must have a static 'From' method.");

        ValueProperty = typeof(TValueObject).GetProperty("Value", BindingFlags.Public | BindingFlags.Instance)
            ?? throw new InvalidOperationException($"The type {typeof(TValueObject)} must have a 'Value' property.");
    }

    public override void SetValue(IDbDataParameter parameter, TValueObject valueObject)
    {
        parameter.DbType = DbType.Int32;
        parameter.Value = ValueProperty.GetValue(valueObject) ?? DBNull.Value;
    }

    public override TValueObject Parse(object? value)
    {
        if (value is null or DBNull)
        {
            return default;
        }

        if (value is int intValue)
        {
            return (TValueObject)(FromMethod.Invoke(null, [intValue])
                ?? throw new InvalidOperationException($"Method {typeof(TValueObject)}.From(int value) returned null."));
        }

        throw new InvalidCastException($"Unable to cast value of type {value.GetType()} to {typeof(TValueObject)}");
    }
}

then, it is applied for all value objects wrapping an int and having the GeneratedCodeAttribute with Tool=="Vogen" using reflection:

var strongIdTypes = assembly.GetTypes()
            .Where(
                type => type is { IsValueType: true, IsPrimitive: false }
                    && typeof(IEquatable<int>).IsAssignableFrom(type)
                    && type.GetCustomAttributes<GeneratedCodeAttribute>().Any(attr => attr.Tool == "Vogen"))
            .ToArray();

        foreach (var strongIdType in strongIdTypes)
        {
            var handlerType = typeof(StrongIdValueTypeHandler<>).MakeGenericType(strongIdType);
            object handler = Activator.CreateInstance(handlerType)
                ?? throw new InvalidOperationException($"Could not create an instance of {handlerType}");

            SqlMapper.AddTypeHandler(strongIdType, (SqlMapper.ITypeHandler) handler);
        }

This way, no code generation is required, and the overhead is neglectable since the reflection results are cached. What do you think? ChatGPT liked it :smile:

marcwittke avatar Jan 03 '25 22:01 marcwittke

Thanks for the feedback @marcwittke - I intend to add the ability to generate most of the converters/serialisers into the project where they're declared rather than the project where the value object is declared.

Thanks for suggesting your approach. However, I wouldn't want to add any Reflection code as I think it'd break a fair few users who use Vogen in trimmed/AOT apps (I know I'd break my own projects if I did this).

It doesn't look to difficult to implement, so I'll attempt it at some point in the new couple of weeks.

Thanks again,

Steve

SteveDunn avatar Jan 08 '25 22:01 SteveDunn