dotnet/reactive · error · InvalidOperationException

The delegate type for an event conforming to the traditional

Error message

The delegate type for an event conforming to the traditional event pattern should take two parameters.

What it means

FromEventPattern validates that the delegate type used to bridge a .NET event to an async observable follows the traditional (sender, eventArgs) two-parameter pattern. GetEventMethods reflects over the delegate's Invoke method and throws this InvalidOperationException when the parameter count differs from 2, because the adapter cannot map the event payload onto TSender/TEventArgs otherwise.

Solutions

  1. Use a delegate type with exactly two parameters: (object? sender, TEventArgs e), e.g. EventHandler<TEventArgs> or a matching custom delegate
  2. If the event has a non-standard signature, use the overload taking explicit add/remove handler functions (Action<EventHandler<TEventArgs>> add, ...) instead of reflection-based event lookup
  3. Verify the target event's delegate type with typeof(EventDelegate).GetMethod("Invoke").GetParameters().Length before calling

Example fix

// before
AsyncObservable.FromEventPattern<MyEventArgs>(e => src.CustomEvent += e, e => src.CustomEvent -= e); // delegate takes 1 param
// after
AsyncObservable.FromEventPattern<object, MyEventArgs>(h => (s, e) => h(s, e), e => src.MyEvent += e, e => src.MyEvent -= e);
Defensive patterns

Strategy: validation

Validate before calling

var invoke = typeof(MyDelegate).GetMethod("Invoke");
if (invoke.GetParameters().Length != 2)
    throw new InvalidOperationException("Event delegate must take (sender, args)");

Type guard

static bool IsTraditionalEventDelegate<TSender, TEventArgs>(Type d) where TSender : class
    => d.GetMethod("Invoke") is { } m
       && m.GetParameters().Length == 2
       && typeof(TSender).IsAssignableFrom(m.GetParameters()[0].ParameterType)
       && typeof(TEventArgs).IsAssignableFrom(m.GetParameters()[1].ParameterType)
       && m.ReturnType == typeof(void);

Try / catch

try
{
    var obs = AsyncObservable.FromEventPattern<TSender, TEventArgs>(add, remove);
}
catch (InvalidOperationException ex) when (ex.Message.Contains("two parameters"))
{
    // fall back to FromEvent-style add/remove handler overload
}

Prevention

When it happens

Trigger: Calling FromEventPattern (or the GetEventMethods helper) with a delegate type whose Invoke method takes 0, 1, or 3+ parameters — e.g. Action (no args), EventHandler that was replaced by a single-argument 'e => {}' lambda-style handler, or a custom delegate with extra parameters.

Common situations: Migrating from classic Rx FromEventPattern to AsyncRx where events use Action-style or custom delegates; picking the wrong overload that infers a non-standard delegate type; modern events that abandon the (object sender, EventArgs e) convention.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of dotnet/reactive@94b5d5ab91 (2026-09-15). Data as JSON: /api/errors/bed968d2beb8b1b2. Report an issue: GitHub.

Appendix: source

Thrown at AsyncRx.NET/System.Reactive.Async/Linq/Operators/FromEventPattern.cs:313

            var isWinRT = false;

            if (addMethod.ReturnType != typeof(void))
            {
                isWinRT = true;

                var pet = psr[0];
                if (pet.ParameterType != addMethod.ReturnType)
                    throw new InvalidOperationException("An event should either have add and remove methods that return void or an add method that returns a type compatible with the parameter type of the remove method.");
            }

            var delegateType = psa[0].ParameterType;

            var invokeMethod = delegateType.GetMethod("Invoke");

            var parameters = invokeMethod.GetParameters();

            if (parameters.Length != 2)
                throw new InvalidOperationException("The delegate type for an event conforming to the traditional event pattern should take two parameters.");

            if (!typeof(TSender).IsAssignableFrom(parameters[0].ParameterType))
                throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "The sender parameter of the event is not assignable to '{0}'.", typeof(TSender).FullName));

            if (!typeof(TEventArgs).IsAssignableFrom(parameters[1].ParameterType))
                throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "The event arguments parameter of the event is not assignable to '{0}'.", typeof(TEventArgs).FullName));

            if (invokeMethod.ReturnType != typeof(void))
                throw new InvalidOperationException("The return type of an event delegate should be void.");

            return (addMethod, removeMethod, delegateType, isWinRT);
        }

        public static EventInfo GetEventEx(this Type type, string name, bool isStatic)
        {
            return type.GetEvent(name, isStatic ? BindingFlags.Public | BindingFlags.Static : BindingFlags.Public | BindingFlags.Instance);
        }
    }

View on GitHub (pinned to 94b5d5ab91)