vendredi 1 décembre 2023

Move the logic to separate handlers

I have the helper class which translate list TreeField structures to some generic type, and I have a problem with initializing the fields. More precisely, I want to put the logic for each type into separate handlers so that I can add them without changing the logic of ExpressionHelper itself.

What exactly i wanna separate.

var propertyType = property.PropertyType;

if (propertyType.IsPrimitive || propertyType == typeof(string))
{
    var memberExpression = Expression.Property(parameter, property.Name);

    var bind = Expression.Bind(property, memberExpression);

    bindings.Add(bind);
}
else if (type.IsClass)
{
    var memberExpression = Expression.Property(parameter, property.Name);

    var subFieldsInit = GetMemberInitExpression(property.PropertyType, memberExpression, field.Children);

    var bind = Expression.Bind(property, subFieldsInit);

    bindings.Add(bind);
}
else if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>))
{
    var elementType = property.PropertyType
        .GetGenericArguments()
        .FirstOrDefault() ?? throw new InvalidOperationException($"Generic type for collection {property.Name} not founded.");

    var selectMethod = typeof(Enumerable).GetMethods()
        .Where(m => m.Name == nameof(Enumerable.Select))
        .First(m => m.GetParameters().Length == 2)
        .MakeGenericMethod(elementType, elementType);

    var lambdaParameter = Expression.Parameter(elementType);

    var lambdaBody = GetMemberInitExpression(elementType, lambdaParameter, field.Children);

    var selectLambda = Expression.Lambda(lambdaBody, lambdaParameter);

    var memberAccess = Expression.Property(parameter, property);

    var call = Expression.Call(selectMethod, memberAccess, selectLambda);

    var bind = Expression.Bind(property, call);

    bindings.Add(bind);
}

Is there any pattern that fits my situation or should I structure the code differently? All other code.

public readonly struct TreeField(string name, IEnumerable<TreeField> children)
{
    public required string Name { get; init; } = name ?? string.Empty;

    public IEnumerable<TreeField> Children { get; init; } = children ?? Enumerable.Empty<TreeField>();
}

public sealed class ExpressionHelper
{
    public Expression<Func<TEntity, TEntity>> GetLambdaExpression<TEntity>(IEnumerable<TreeField> fields)
    {
        var parameter = Expression.Parameter(typeof(TEntity));

        var result = GetMemberInitExpression(typeof(TEntity), parameter, fields);

        return Expression.Lambda<Func<TEntity, TEntity>>(result, parameter);
    }

    private MemberInitExpression GetMemberInitExpression(Type type, Expression parameter, IEnumerable<TreeField> fields)
    {
        var bindings = new List<MemberBinding>();

        foreach (var field in fields)
        {
            var property = type.GetProperty(field.Name, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);

            if (property is not null)
            {
                var propertyType = property.PropertyType;

                if (propertyType.IsPrimitive || propertyType == typeof(string))
                {
                    var memberExpression = Expression.Property(parameter, property.Name);

                    var bind = Expression.Bind(property, memberExpression);

                    bindings.Add(bind);
                }
                else if (type.IsClass)
                {
                    var memberExpression = Expression.Property(parameter, property.Name);

                    var subFieldsInit = GetMemberInitExpression(property.PropertyType, memberExpression, field.Children);

                    var bind = Expression.Bind(property, subFieldsInit);

                    bindings.Add(bind);
                }
                else if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>))
                {
                    var elementType = property.PropertyType
                        .GetGenericArguments()
                        .FirstOrDefault() ?? throw new InvalidOperationException($"Generic type for collection {property.Name} not founded.");

                    var selectMethod = typeof(Enumerable).GetMethods()
                        .Where(m => m.Name == nameof(Enumerable.Select))
                        .First(m => m.GetParameters().Length == 2)
                        .MakeGenericMethod(elementType, elementType);

                    var lambdaParameter = Expression.Parameter(elementType);

                    var lambdaBody = GetMemberInitExpression(elementType, lambdaParameter, field.Children);

                    var selectLambda = Expression.Lambda(lambdaBody, lambdaParameter);

                    var memberAccess = Expression.Property(parameter, property);

                    var call = Expression.Call(selectMethod, memberAccess, selectLambda);

                    var bind = Expression.Bind(property, call);

                    bindings.Add(bind);
                }
            }
        }

        return Expression.MemberInit(Expression.New(type), bindings);
    }
}

I tried to do this with something like a strategy pattern. But I had a problem with passing

Func<Type, Expression, IEnumerable<TreeField>, MemberInitExpression> memberInit 

into the strategy.

public sealed class BindingContext : IBindingContext
{
    private readonly IEnumerable<IBindingStrategy> strategies;

    public BindingContext(IEnumerable<IBindingStrategy> strategies)
    {
        this.strategies = strategies;
    }

    public MemberBinding Bind(PropertyInfo property, Expression parameter, TreeField field, Func<Type, Expression, IEnumerable<TreeField>, MemberInitExpression> memberInit)
    {
        var strategy = strategies.FirstOrDefault(s => s.AppliesTo(property.PropertyType))
            ?? throw new NullReferenceException($"Binding startegy for {property.PropertyType} not registered.");

        return strategy.Bind(property, parameter, field, memberInit);
    }
}
public sealed class EntityStrategy : IBindingStrategy
{
    public bool AppliesTo(Type type) => type.IsClass && type != typeof(string);

    public MemberBinding Bind(PropertyInfo property, Expression parameter, TreeField field, Func<Type, Expression, IEnumerable<TreeField>, MemberInitExpression> memberInit)
    {
        var memberExpression = Expression.Property(parameter, property.Name);

        var subFieldsInit = memberInit(property.PropertyType, memberExpression, field.Children);

        return Expression.Bind(property, subFieldsInit);
    }
}

Aucun commentaire:

Enregistrer un commentaire