Invoking a non-generic validation method in OnMethodBoundaryAspect.OnEntry when it belongs to a closed generic class throws exception with "Late bound operations cannot be performed on types or methods for which ContainsGenericParameters is true"

onurer's Avatar

onurer

07 Aug, 2018 08:17 PM

This problem is not directly related to PostSharp but I couldn't find a solution after excessive amount of research and I experience the problem in a PostSharp scenario so, I decided to ask about it here.

I have a decent amount of experience with implementing custom PostSharp aspects. I have a custom method boundary aspect called ValidateOperationAttribute. It takes a method name as a parameter and calls the method (validation method) in OnEntry of the aspect which is applied to another method (validated method).

This implementation was working fine for a long time but recently I experienced a problem when I convert the base class of the actual class which contains both validation and validated methods to generic type. So, the methods are NOT generic. Only the base class of their class is generic.

In this new scenario, I get the exception below when I invoke the validation method in OnEntry of validated method.

"InvalidOperationException: Late bound operations cannot be performed on types or methods for which ContainsGenericParameters is true."

I researched the web for long long time and I have tried many possible/suggested examples but I couldn't find the actual solution. All of the examples are dealing with generic methods. In my case, the method is NOT generic. The class which contains the method is NOT generic. The class inherits a base class and only the base class IS generic.

Because of this reason, my custom method validation aspect can't be used in this scenario which I really need.

Can anyone give me any idea on invoking a method which belongs to a closed (non-generic) class which inherits from a generic base class please?

I'll try to isolate the scenario and upload the trimmed code files so you can see what I mean.

  1. 1 Posted by onurer on 07 Aug, 2018 08:22 PM

    onurer's Avatar

    Here is the method validation aspect:

        using System.Reflection;
        using PostSharp.Aspects;
        using PostSharp.Extensibility;
        using PostSharp.Serialization;
    
        namespace Atesh
        {
            [PSerializable]
            public class ValidateOperationAttribute : OnMethodBoundaryAspect, IInstanceScopedAspect
            {
                public string Method;
    
                MethodInfo ValidationMethod;
                object Instance;
    
                public ValidateOperationAttribute() => AspectPriority = 100;
    
                public override void CompileTimeInitialize(MethodBase Method, AspectInfo AspectInfo) => ValidationMethod = Method.DeclaringType.GetMethod(this.Method ?? "", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
    
                public override bool CompileTimeValidate(MethodBase Method)
                {
                    if (ValidationMethod == null)
                    {
                        Message.Write(MessageLocation.Of(Method), SeverityType.Fatal, "Atesh", Strings.CouldntFindValidationMethod(this.Method));
    
                        return false;
                    }
    
                    return true;
                }
    
                public object CreateInstance(AdviceArgs AdviceArgs)
                {
                    Instance = AdviceArgs.Instance;
    
                    return MemberwiseClone();
                }
    
                public void RuntimeInitializeInstance() { }
    
                // Sealed for PostSharp performance warning.
                public sealed override void OnEntry(MethodExecutionArgs Args)
                {
                    try
                    {
                        ValidationMethod.Invoke(Instance, null);
                    }
                    catch (TargetInvocationException E)
                    {
                        throw E.InnerException;
                    }
                }
            }
        }
    
  2. 2 Posted by onurer on 07 Aug, 2018 08:32 PM

    onurer's Avatar

    The abstract generic base class (highly trimmed the code) which contains the validation and validated methods:

        public abstract class Waitress<T>
        {
            void ValidateRunner()
            {
                if (Runner != null) throw new InvalidOperationException();
            }
    
            [ValidateOperation(Method = "ValidateRunner")]
            protected Coroutine StartRunner()
            {
                return StartCoroutine(Runner);
            }
        }
    

    The actual class which I create an instance of and call the StartRunner method on its instance:

        public class Conditioner : Waitress<AbortDoneEventArgs>
        {
            public Coroutine Wait()
            {
                return StartRunner();
            }
        }
    
  3. 3 Posted by onurer on 07 Aug, 2018 11:16 PM

    onurer's Avatar

    I asked the same question without the PostSharp context on StackOverflow. If you wanna answer it there:

    https://stackoverflow.com/questions/51736656/invoking-a-non-generic...

  4. Support Staff 4 Posted by PostSharp Techn... on 08 Aug, 2018 09:23 AM

    PostSharp Technologies's Avatar

    Hello,

    glad you have found the solution. Just a note for the PostSharp context - in this specific case you have provided you can completely avoid reflection by importing the method into the aspect:

    [PSerializable]
    public class ValidateOperationAttribute : OnMethodBoundaryAspect, IInstanceScopedAspect, IAdviceProvider
    {
        public string Method;
    
        public Action ValidationMethod;
    
        public ValidateOperationAttribute() => AspectPriority = 100;
        
        public object CreateInstance(AdviceArgs AdviceArgs)
        {
            return MemberwiseClone();
        }
    
        public void RuntimeInitializeInstance() { }
    
        public sealed override void OnEntry(MethodExecutionArgs Args)
        {
                ValidationMethod();
        }
    
        public IEnumerable<AdviceInstance> ProvideAdvices(object targetElement)
        {
            yield return new ImportMethodAdviceInstance(this.GetType().GetField(nameof(this.ValidationMethod), BindingFlags.Public | BindingFlags.Instance), this.Method);
        }
    }
    

    It may not be useful for your actual use case, but works fine for the above code.

    Hope it helps.

    Best regards,
    Daniel

  5. 5 Posted by onurer on 08 Aug, 2018 04:04 PM

    onurer's Avatar

    Hi. Thank you for your answer. I am interested in it if it makes more sense to import the method into the aspect than using reflection. I want to understand what's going on in your solution.

    First thing I noticed is you removed my compile time init and validate methods. Why was that?
    Second, I don't know about the ProvideAdvices. What does it do? Is it for compile time or runtime?

    What happens in compile time and runtime when you import a method like that?

  6. Support Staff 6 Posted by PostSharp Techn... on 08 Aug, 2018 06:16 PM

    PostSharp Technologies's Avatar

    Hello,

    this is all compile time logic, which why I was suggesting it, it's going to have better performance. IAdviceProvider is a compile time interface called by PostSharp, enabling you to dynamically provide advices (which weave some code to the target class) such as importing of the method into the aspect. I have removed CompileTimeValidate for brevity - in case the method specified in ProvideAdvices is not found or is null, PostSharp would produce an error by itself. It's lower level than IAspectProvider that allows you to push other aspects to the target declaration.

    What happens at runtime is that when the aspect instance is created (along with the target instance it is based on) a new delegate pointing to the target instance and the specified method is created and set to the field in the aspect. There is no reflection involved at runtime, neither during creation of object nor when the method is executed. The downside is that you need to know signature of the method ahead of time.

    Internally ldftn IL instruction is used to create the delegate, which is something like typeof in C# but for methods. It is translated by the JIT to an actual method on the closed generic type Conditioner even though it is used in context of Waitress<T>.

    If you have further questions, please don't hesitate to ask :-).

    Best regards,
    Daniel

  7. 7 Posted by onurer on 09 Aug, 2018 12:44 AM

    onurer's Avatar

    Thanks for detailed explanation.

    This is my latest implementation. As you can see, I am searching the base type chain to find the correct method since I don't know which one it belongs to.

    Do you think I should implement such logic if I use the way you suggested?

            public void RuntimeInitializeInstance()
            {
                // The ContainsGenericParameters property searches recursively for type parameters. If it returns true we need to search the base type chain.
                if (ValidationMethod.ContainsGenericParameters)
                {
                    var BaseType = Instance.GetType().BaseType;
    
                    while (BaseType != null && ValidationMethod != null && ValidationMethod.ContainsGenericParameters)
                    {
                        ValidationMethod = BaseType.GetMethod(Method, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
    
                        BaseType = BaseType.BaseType;
                    }
                }
            }
    
            // Sealed for PostSharp performance warning.
            public sealed override void OnEntry(MethodExecutionArgs Args)
            {
                try
                {
                    ValidationMethod.Invoke(Instance, null);
                }
                catch (TargetInvocationException E)
                {
                    throw E.InnerException;
                }
            }
    
  8. Support Staff 8 Posted by PostSharp Techn... on 09 Aug, 2018 08:13 AM

    PostSharp Technologies's Avatar

    Hello,

    I think it is unnecessary even in case of reflection. The problem you were having was that you looked for the method in CompileTimeInitialize which is running in the context of the generic type definition and thus the MethodInfo you got from GetMethod could not be executed at runtime as it was "declared" by an open generic type. If you move the logic to RuntimeInitializeInstance, just calling GetMethod on Instance.GetType() should be enough and you would get a method on the actual JITtable (or JITted) type corresponding to the actual instance. When BindingFlags.DeclaredOnly is not specified reflection includes the base type in the search (only GetNestedTypes does not do that).

    Depending on your scenario, I would suggest either using the "dynamic" (it's still compile time) method import described earlier, or caching the MethodInfo for the actual type of the instance in a ConcurrentDictionary<Type, MethodInfo> as these reflection searches in RuntimeInitializeInstance would be quite costly.

    Best regards,
    Daniel

  9. 9 Posted by onurer on 09 Aug, 2018 01:29 PM

    onurer's Avatar

    Ok. I wanna do that but I have questions.

    1) I can't see where you set the value of ValidationMethod in your example. How?

    2) Also I get this message from ImportMethodAdviceInstance call:
    "PostSharp.Aspects.Advices.ImportMethodAdviceInstance" is not valid on "Atesh.ValidateOperationAttribute.ValidationMethod" because this member is not public"

    3) Why does ImportMethodAdviceInstance have a FieldInfo parameter instead of MethodInfo?

        [PSerializable]
        public class ValidateOperationAttribute : OnMethodBoundaryAspect, IInstanceScopedAspect, IAdviceProvider
        {
            public string Method;
    
            Action ValidationMethod;
    
            public ValidateOperationAttribute() => AspectPriority = 100;
    
            public IEnumerable<AdviceInstance> ProvideAdvices(object TargetElement)
            {
                yield return new ImportMethodAdviceInstance(GetType().GetField(nameof(ValidationMethod), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static), Method);
            }
    
            public object CreateInstance(AdviceArgs AdviceArgs) => MemberwiseClone();
    
            public void RuntimeInitializeInstance()
            {
            }
    
            // Sealed for PostSharp performance warning.
            public sealed override void OnEntry(MethodExecutionArgs Args) => ValidationMethod();
        }
    
  10. Support Staff 10 Posted by PostSharp Techn... on 09 Aug, 2018 02:31 PM

    PostSharp Technologies's Avatar

    Hello,

    1) ValidationMethod is set automatically when the aspect instance is created. This is done in <>z__InitializeAspects method that is added to the target class and called from the constructor. It looks like this:

    // Create the aspect and set it to the field on the target class.
    <>z__aspect1 = (ValidateOperationAttribute)<>z__a_1.a1.CreateInstance(null);
    // Initialize the field containing delegate using method on the current class.
    <>z__aspect1.ValidationMethod = this.Validate; 
    // Run runtime initialize instance.
    <>z__aspect1.RuntimeInitializeInstance();
    

    2) The field needs to be public because as you see in the above code, it needs to be set from the target class.

    3) The first argument of the advice is the destination field on the aspect, the second is method name. It could be MethodInfo but the current design uses name of the method and the signature is inferred from the delegate type that is used for the field.

    Additionally you need to set the third argument isRequired to true otherwise you would not get an error if the method does not exist and the delegate would be null at runtime. I apologize for misleading.

    Best regards,
    Daniel

  11. 11 Posted by onurer on 09 Aug, 2018 09:11 PM

    onurer's Avatar

    Hi again. Please my latest version below.

    1) I don't get a compiler error when the method (MethodName's value) doesn't exist.

    2) When the method exists, I get null exception in OnEntry. Probably Validate is null.

    3) Since I don't need AdviceArgs.Instance anymore, is it still required to implement the IInstanceScopedAspect interface? Can I just delete it with its methods together (CreateInstance, RuntimeInitializeInstance).

        [PSerializable]
        public class ValidateOperationAttribute : OnMethodBoundaryAspect, IInstanceScopedAspect
        {
            public string MethodName;
            public Action Validate;
    
            public ValidateOperationAttribute() => AspectPriority = 100;
    
            public object CreateInstance(AdviceArgs AdviceArgs) => MemberwiseClone();
    
            public void RuntimeInitializeInstance()
            {
            }
    
            public IEnumerable<AdviceInstance> ProvideAdvices(object TargetElement)
            {
                yield return new ImportMethodAdviceInstance(GetType().GetField(nameof(Validate), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static), MethodName, true);
            }
            
            // Sealed for PostSharp performance warning.
            public sealed override void OnEntry(MethodExecutionArgs Args) => Validate();
        }
    
  12. Support Staff 12 Posted by PostSharp Techn... on 10 Aug, 2018 08:32 AM

    PostSharp Technologies's Avatar

    Hello,

    you are missing IAdviceProvider interface in the code you have pasted. Could it be the problem?

    Best regards,
    Daniel

  13. 13 Posted by onurer on 10 Aug, 2018 06:56 PM

    onurer's Avatar

    It's working. Thank you.

    I love PostSharp and AOP so much!

  14. Support Staff 14 Posted by PostSharp Techn... on 10 Aug, 2018 08:06 PM

    PostSharp Technologies's Avatar

    Hello,

    you're welcome. I love it too :-).

    Best regards,
    Daniel

  15. Support Staff 15 Posted by PostSharp Techn... on 10 Aug, 2018 08:06 PM

    PostSharp Technologies's Avatar

    Hello,

    We are going to close this request as we believe it was solved. Please feel free to reopen the discussion if you need more help.

    Best regards,
    Daniel

  16. PostSharp Technologies closed this discussion on 10 Aug, 2018 08:06 PM.

Comments are currently closed for this discussion. You can start a new one.

Keyboard shortcuts

Generic

? Show this help
ESC Blurs the current field

Comment Form

r Focus the comment reply box
^ + ↩ Submit the comment

You can use Command ⌘ instead of Control ^ on Mac