dotnet / dotnet/runtime

Registering contravariant event handlers can throw at runtime

Open
#131,774 3 comments 0 reactions 0 assignees View on GitHub
area-TypeSystem-coreclr untriaged
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.