Skip to content

"Expression is not an event add" when using .Raises() #1175

Closed

Description

@howcheng

When trying to have a mocked method raise an event, I'm getting the error message "Expression is not an event add". The interface I'm using is a mess of inheritance and new versions of events, so maybe that's related, but here's a fiddle with a stripped-down version where you can see the error happening.

https://dotnetfiddle.net/kqhOln

Did I do something wrong, or is this a bug?

Thanks.

Activity

  1. tonyhallett commented on Jun 24, 2021

    @tonyhallett
    Contributor

    When DoSomething is called the corresponding
    MethodCall ExecuteCore will Execute the RaiseEvent Behaviour
    which calls Mock.RaiseEvent

    https://github.com/moq/moq4/blob/a6fde8b6d79a7437bf642d115785b97f40779b6a/src/Moq/Mock.cs#L688

    
    		internal static void RaiseEvent(Mock mock, LambdaExpression expression, Stack<InvocationShape> parts, object[] arguments)
    		{
    			const BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly;
    
    			var part = parts.Pop();
    			var method = part.Method;
    
    			if (parts.Count == 0)
    			{
    				EventInfo @event;
    				if (method.IsEventAddAccessor())
    				{
    					var implementingMethod = method.GetImplementingMethod(mock.Object.GetType());
    					@event = implementingMethod.DeclaringType.GetEvents(bindingFlags).SingleOrDefault(e => e.GetAddMethod(true) == implementingMethod);
    					if (@event == null) //************************************
    					{
    						throw new ArgumentException(
    							string.Format(
    								CultureInfo.CurrentCulture,
    								Resources.SetupNotEventAdd,
    								part.Expression));
    					}
    				}
    
    

    There is similar code hit from SetupAdd which should add the handler to be called in Mock.RaiseEvent

    https://github.com/moq/moq4/blob/a6fde8b6d79a7437bf642d115785b97f40779b6a/src/Moq/Interception/InterceptionAspects.cs#L133

    	internal static class HandleEventSubscription
    	{
    		public static bool Handle(Invocation invocation, Mock mock)
    		{
    			const BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly;
    
    			var methodName = invocation.Method.Name;
    
    			// Special case for event accessors. The following, seemingly random character checks are guards against
    			// more expensive checks (for the common case where the invoked method is *not* an event accessor).
    			if (methodName.Length > 4)
    			{
    				if (methodName[0] == 'a' && methodName[3] == '_' && invocation.Method.IsEventAddAccessor())
    				{
    					var implementingMethod = invocation.Method.GetImplementingMethod(invocation.ProxyType);
    					var @event = implementingMethod.DeclaringType.GetEvents(bindingFlags).SingleOrDefault(e => e.GetAddMethod(true) == implementingMethod);
    // ********************************************************** event is null 
    					if (@event != null)
    					{
    						if (mock.CallBase && !invocation.Method.IsAbstract)
    						{
    							invocation.ReturnValue = invocation.CallBase();
    							return true;
    						}
    						else if (invocation.Arguments.Length > 0 && invocation.Arguments[0] is Delegate delegateInstance)
    						{
    //********* this is what expect to occur
    							mock.EventHandlers.Add(@event, delegateInstance);
    							return true;
    						}
    					}
    				}
    

    if we look at the events on the proxy ( note that I have pasted your code into Moq tests )
    implementingMethod.DeclaringType.GetEvents(bindingFlags)

    MessageReceived
    System.EventHandler`1[[Moq.Tests.EventHandlersFixture+ItemEventArgs`1[[System.Object, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Moq.Tests, Version=1.0.0.0, Culture=neutral, PublicKeyToken=69f491c39445e920]]
    
    IReadableQueue`1[Object].MessageReceived
    System.EventHandler`1[[Moq.Tests.EventHandlersFixture+ItemEventArgs`1[[System.Object, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Moq.Tests, Version=1.0.0.0, Culture=neutral, PublicKeyToken=69f491c39445e920]]
    
    

    Which disagrees with the single event declared on the ISqsQueueClient<object> type

    MessageReceived
    System.EventHandler`1[[Moq.Tests.EventHandlersFixture+SqsMessageEventArgs`1[[System.Object, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Moq.Tests, Version=1.0.0.0, Culture=neutral, PublicKeyToken=69f491c39445e920]]
    
    

    I agree that it is a mess of inheritance and new versions of events but it still looks like a bug to me ( in Castle.Core.)

    @stakx Your thoughts

  2. tonyhallett commented on Jun 25, 2021

    @tonyhallett
    Contributor

    All 3 events have accessors on the proxy

    image

  3. stakx commented on Jun 28, 2021

    @stakx
    Contributor

    @howcheng, thanks for reporting this issue. And thanks @tonyhallett for doing some preliminary investigating.

    First, to simplify things a little, let's use a more minimal repro (derived from the original one above mostly through a process of elimination):

    using System;
    using Moq;
    
    Mock<IDerived> mock = new Mock<IDerived>();
    mock.Setup(x => x.RaiseEvent()).Raises(x => x.Event += null, false);
    mock.Object.Event += _ => { Console.WriteLine("Hello world!!!"); };
    mock.Object.RaiseEvent();
    
    public interface IBase
    {
        event Action Event;
        void RaiseEvent();
    }
    
    public interface IDerived : IBase
    {
        new event Action<bool> Event;
    }

    I've played around with this a bit, and the problem appears to be triggered by having two events of the same name but different signature, spread across two types which are related via subtyping.

    I've taken a closer look at what's going on under the hood. Among other things, I've used a modified version of Moq that stores DynamicProxy's dynamic assembly to disk, then I inspected that assembly using ildasm / ilspy. It turns out that DynamicProxy generates a proxy class having two events with an identical name:

    .event [mscorlib]System.Action Event
    {
        .addon instance void Castle.Proxies.IDerivedProxy::add_Event(class [mscorlib]System.Action)
        .removeon instance void Castle.Proxies.IDerivedProxy::remove_Event(class [mscorlib]System.Action)
    }
    
    .event class [mscorlib]System.Action`1<bool> Event
    {
        .addon instance void Castle.Proxies.IDerivedProxy::add_Event(class [mscorlib]System.Action`1<bool>)
        .removeon instance void Castle.Proxies.IDerivedProxy::remove_Event(class [mscorlib]System.Action`1<bool>)
    }

    This kind of overloading is illegal, as peverify will confirm:

    [MD]: Error: Event has a duplicate (token=0x14000002). [token:0x14000001]
    [MD]: Error: Event has a duplicate (token=0x14000001). [token:0x14000002]
    2 Error(s) Verifying CastleDynProxy2.dll
    

    This event duplication then causes Reflection to report incorrect results, making the following .SingleOrDefault query in Moq fail:

    https://github.com/moq/moq4/blob/a6fde8b6d79a7437bf642d115785b97f40779b6a/src/Moq/Mock.cs#L687

    ... which in turn causes the error message initially reported.

    We'll likely need to get this fixed in DynamicProxy first before we can do anything in Moq (if any change will be necessary after patching DynamicProxy).

  4. toppiovi commented on Jul 13, 2021

    @toppiovi

    Here is another instance of Raise() failing when the base class event is hidden by a new generic child class event.
    Interestingly, this issue was not present in Moq 4.5.3, but started occurring when updating to Moq 4.16.1
    I hope providing this small example helps!

        public interface IEvents
        {
            event EventHandler Created;
        }
    
        public interface IGenericHidingEvents<T> : IEvents
        {
            new event EventHandler<T> Created;
        }
    
    
        public class GenericHidingEventConsumer
        {
            private IGenericHidingEvents<bool> myHidingEvents;
    
            public GenericHidingEventConsumer(IGenericHidingEvents<bool> theHidingEvents)
            {
                this.myHidingEvents = theHidingEvents;
                this.myHidingEvents.Created += this.HidingEventsOnCreated;
            }
    
            private void HidingEventsOnCreated(object theSender, bool theE)
            {
                EventHandled = true;
            }
    
            public bool EventHandled { get; set; }
        }
    
        [TestClass]
        public class GenericHidingEventsConsumerTests
        {
            [TestMethod]
            public void created_event_is_handled()
            {
                var aEventsMock = new Mock<IGenericHidingEvents<bool>>();
                var aConsumer = new GenericHidingEventConsumer(aEventsMock.Object);
    
                aEventsMock.Raise(theO => theO.Created += null, true);
    
    //     Test method playground.GenericHidingEventsConsumerTests.created_event_is_handled threw exception: 
    //     System.ArgumentException: Expression is not an event add: theO => theO.add_Created(null)_
    
                Assert.IsTrue(aConsumer.EventHandled); // Fails
            }
        }
    
  5. added this to the 4.18.2 milestone on Aug 2, 2022
  6. stakx commented on Aug 2, 2022

    @stakx
    Contributor

    @toppiovi, FYI, your test case has a slight problem that would prevent it from passing even if there were no issue with Moq:

    -aEventsMock.Raise(theO => theO.Created += null, true);
    +aEventsMock.Raise(theO => theO.Created += null, /* sender */, true);

    I'm going to add a corrected version of your test case to the regression suite.

  7. stakx commented on Aug 2, 2022

    @stakx
    Contributor

    I've just pushed Moq version 4.18.2 to NuGet, that version includes a bugfix for this issue. Thanks again for reporting!

  8. locked and limited conversation to collaborators on Sep 5, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions