{"record":{"id":"bed968d2beb8b1b2","repo":"dotnet/reactive","slug":"the-delegate-type-for-an-event-conforming-to-the-traditional","errorCode":null,"errorMessage":"The delegate type for an event conforming to the traditional event pattern should take two parameters.","messagePattern":"The delegate type for an event conforming to the traditional event pattern should take two parameters\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"AsyncRx.NET/System.Reactive.Async/Linq/Operators/FromEventPattern.cs","lineNumber":313,"sourceCode":"            var isWinRT = false;\n\n            if (addMethod.ReturnType != typeof(void))\n            {\n                isWinRT = true;\n\n                var pet = psr[0];\n                if (pet.ParameterType != addMethod.ReturnType)\n                    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.\");\n            }\n\n            var delegateType = psa[0].ParameterType;\n\n            var invokeMethod = delegateType.GetMethod(\"Invoke\");\n\n            var parameters = invokeMethod.GetParameters();\n\n            if (parameters.Length != 2)\n                throw new InvalidOperationException(\"The delegate type for an event conforming to the traditional event pattern should take two parameters.\");\n\n            if (!typeof(TSender).IsAssignableFrom(parameters[0].ParameterType))\n                throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, \"The sender parameter of the event is not assignable to '{0}'.\", typeof(TSender).FullName));\n\n            if (!typeof(TEventArgs).IsAssignableFrom(parameters[1].ParameterType))\n                throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, \"The event arguments parameter of the event is not assignable to '{0}'.\", typeof(TEventArgs).FullName));\n\n            if (invokeMethod.ReturnType != typeof(void))\n                throw new InvalidOperationException(\"The return type of an event delegate should be void.\");\n\n            return (addMethod, removeMethod, delegateType, isWinRT);\n        }\n\n        public static EventInfo GetEventEx(this Type type, string name, bool isStatic)\n        {\n            return type.GetEvent(name, isStatic ? BindingFlags.Public | BindingFlags.Static : BindingFlags.Public | BindingFlags.Instance);\n        }\n    }","sourceCodeStart":295,"sourceCodeEnd":331,"githubUrl":"https://github.com/dotnet/reactive/blob/94b5d5ab912789f5abe9a72138a25bbd716fe59c/AsyncRx.NET/System.Reactive.Async/Linq/Operators/FromEventPattern.cs#L295-L331","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Use a delegate type with exactly two parameters: (object? sender, TEventArgs e), e.g. EventHandler<TEventArgs> or a matching custom delegate","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","Verify the target event's delegate type with typeof(EventDelegate).GetMethod(\"Invoke\").GetParameters().Length before calling"],"exampleFix":"// before\nAsyncObservable.FromEventPattern<MyEventArgs>(e => src.CustomEvent += e, e => src.CustomEvent -= e); // delegate takes 1 param\n// after\nAsyncObservable.FromEventPattern<object, MyEventArgs>(h => (s, e) => h(s, e), e => src.MyEvent += e, e => src.MyEvent -= e);","handlingStrategy":"validation","validationCode":"var invoke = typeof(MyDelegate).GetMethod(\"Invoke\");\nif (invoke.GetParameters().Length != 2)\n    throw new InvalidOperationException(\"Event delegate must take (sender, args)\");","typeGuard":"static bool IsTraditionalEventDelegate<TSender, TEventArgs>(Type d) where TSender : class\n    => d.GetMethod(\"Invoke\") is { } m\n       && m.GetParameters().Length == 2\n       && typeof(TSender).IsAssignableFrom(m.GetParameters()[0].ParameterType)\n       && typeof(TEventArgs).IsAssignableFrom(m.GetParameters()[1].ParameterType)\n       && m.ReturnType == typeof(void);","tryCatchPattern":"try\n{\n    var obs = AsyncObservable.FromEventPattern<TSender, TEventArgs>(add, remove);\n}\ncatch (InvalidOperationException ex) when (ex.Message.Contains(\"two parameters\"))\n{\n    // fall back to FromEvent-style add/remove handler overload\n}","preventionTips":["Prefer events whose delegates follow (object? sender, TEventArgs e)","For non-standard events, use add/remove-handler overloads instead of reflection-based FromEventPattern","Sanity-check the delegate's Invoke signature in unit tests"],"tags":["reflection","events","argument-validation"],"backgroundTag":"invalid-argument-value","analyzedSha":"94b5d5ab912789f5abe9a72138a25bbd716fe59c","analyzedAt":"2026-09-15T02:26:24.759Z","contentChangedAt":"2026-09-15T02:26:24.759Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}