Registering contravariant event handlers can throw at runtime
- Dominant language
- C#
- Stars
- 18.3k
- Forks
- 5.6k
- PR merge metrics
- PR metrics pending
Description
### Description
I had a situation where I was trying to delegate an event handler to another one with a more derived argument. This is possible now in .NET 10 because EventHandler is contravariant. This compiles but I discovered that it can throw if you register handlers of different types on different events (see repro).
It looks like the MulticastDelegate being used under the covers is doing a strict type check so it doesn't allow contravariance in this specific case. In the first two lines I can see in the decompiled IL that a `EventHandler` is constructed but in the third line a `EventHandler` is constructed.
### Reproduction Steps
```
// This is a LINQPad script
void Main()
{
Handler += BarHandler; // this works
Handler += FooHandler; // this works
DelegatingHandler += FooHandler; // this throws. works if I comment out the lines above.
Handler?.Invoke(null, new Bar());
}
// You can define other methods, fields, classes and namespaces here
event EventHandler Handler;
event EventHandler DelegatingHandler
{
add => Handler += value;
remove => Handler -= value;
}
void BarHandler(object sender, Bar arg)
{
arg.Dump();
}
void FooHandler(object sender, Foo arg)
{
arg.Dump();
}
class Foo
{
}
class Bar : Foo
{
}
```
### Expected behavior
Doesn't throw.
### Actual behavior
Throws an ArgumentException:
```
Delegates must be of the same type.
at System.MulticastDelegate.CombineImpl(Delegate follow)
at UserQuery.add_Handler(EventHandler`1 value)
at UserQuery.add_DelegatingHandler(EventHandler`1 value), line 16
at UserQuery.Main(), line 5
```
### Regression?
_No response_
### Known Workarounds
_No response_
### Configuration
.NET 10 on Windows 11
### Other information
_No response_
Contributor guide
Research direction
Start by running the LINQPad reproduction on .NET 10 and trace the calls from add_DelegatingHandler through add_Handler to MulticastDelegate.CombineImpl. Compare the delegate types created for Handler and DelegatingHandler. Done means registering FooHandler through DelegatingHandler no longer throws and Handler can still invoke it with a Bar argument.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100