Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate dapper type handlers outside the domain project #737

Open
marcwittke opened this issue Jan 3, 2025 · 2 comments
Open

Generate dapper type handlers outside the domain project #737

marcwittke opened this issue Jan 3, 2025 · 2 comments
Labels
enhancement New feature or request

Comments

@marcwittke
Copy link

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 marcwittke added the enhancement New feature or request label Jan 3, 2025
@marcwittke
Copy link
Author

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 😄

@SteveDunn
Copy link
Owner

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants