audaciaconsulting / Audacia.Core

MIT License
0 stars 0 forks source link

ConvertGenericTypeArgument replaces parameters in nested Lambdas, causing runtime errors #2

Open OwenLacey28 opened 9 months ago

OwenLacey28 commented 9 months ago

Consider the following data model:

public interface IHavePet<TPet> where TPet : IPet
{
    ICollection<TPet> Pets { get; set; }
}

public interface IPet
{
    public string Name { get; set; }
}

public class Dog : IPet
{
    public string Name { get; set; }
}

public class DogOwner : IHavePet<Dog>
{
    public ICollection<Dog> Pets { get; set; } = new List<Dog>();
}

The following code throws an error:

Expression<Func<IHavePet<IPet>, bool>> hasPetCalledToby =
    r => r.Pets.Any(p => p.Name == "Toby");
var dogOwner = new DogOwner()
{
    Pets = new List<Dog>
    {
        new() { Name = "Toby" }
    }
};

var convertedExpression = hasPetCalledToby
    .ConvertGenericTypeArgument<IHavePet<IPet>, DogOwner, bool>()!;
var result = convertedExpression.Compile().Invoke(dogOwner);

Error:

System.NullReferenceException: Object reference not set to an instance of an object. at XXXXXXXXX at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor) at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)

Whereas this works fine:

// Same as above example, but no predicate passed into the `Any`
Expression<Func<IHavePet<IPet>, bool>> hasPet = r => r.Pets.Any();
var dogOwner = new DogOwner()
{
    Pets = new List<Dog>
    {
        new() { Name = "Toby" }
    }
};

var convertedExpression = hasPet
    .ConvertGenericTypeArgument<IHavePet<IPet>, DogOwner, bool>()!;
var result = convertedExpression.Compile().Invoke(dogOwner);

I have narrowed this down to the Expression<Func<IHavePet<IPet>, bool>> containing a nested Lamba. It seems that we're replacing attempting to replace the IPet parameter in the nested lambda as well as the root lambda, resulting in a null reference (not sure where from. image

OwenLacey28 commented 2 weeks ago

Attempted to replicate this again and got the below error message

System.ArgumentException
Property 'System.Collections.Generic.ICollection`1[Audacia.Seed.Tests.Customisations.EntitySeedExtensionTests+IPet] Pets' is not defined for type 'XXX+DogOwner' (Parameter 'property')
   at System.Linq.Expressions.Expression.Property(Expression expression, PropertyInfo property)
   at Audacia.Core.Extensions.Helpers.ParameterReplacer.VisitMember(MemberExpression node)
   at System.Dynamic.Utils.ExpressionVisitorUtils.VisitArguments(ExpressionVisitor visitor, IArgumentProvider nodes)
   at System.Linq.Expressions.ExpressionVisitor.VisitMethodCall(MethodCallExpression node)
   at Audacia.Core.Extensions.Helpers.ParameterReplacer.VisitLambda[T](Expression`1 node)
   at Audacia.Core.Extensions.ExpressionExtensions.ConvertGenericTypeArgument[TSource,TTarget,TResult](Expression`1 root)