aws / aws/aws-dotnet-messaging

HandlerInvoker does not support explicit interface implementation of IMessageHandler<T>.HandleAsync

Open
#325 2 comments 0 reactions 1 assignee Claimed by @normj View on GitHub
bug p2
Dominant language
C#
Stars
143
Forks
27
Avg merge
1d 18h
Merged PRs (30d)
4

Description

### Description

When a message handler implements `IMessageHandler.HandleAsync` as an explicit interface implementation, the framework fails at runtime with:

> Unable to resolve a compatible HandleAsync method for {HandlerType} while handling message ID {MessageEnvelopeId}.

The handler is successfully resolved from DI — the issue is that `HandlerInvoker` uses reflection (`Type.GetMethod`) to find `HandleAsync`, which only searches public methods by default. Explicit interface implementations are private on the implementing type.

### Steps to reproduce

```csharp
public sealed class MyHandler(ILogger logger) : IMessageHandler
{
// Explicit interface implementation — fails at runtime
async Task IMessageHandler.HandleAsync(
MessageEnvelope messageEnvelope,
CancellationToken cancellation)
{
logger.LogInformation("Handling {Id}", messageEnvelope.Message.Id);
return MessageProcessStatus.Success();
}
}
```

Registration:
```csharp
messageBus.AddMessageHandler("MyEvent");
```

### Expected behavior

The handler should work regardless of whether `HandleAsync` is a public method or an explicit interface implementation. Both are valid C# patterns for implementing an interface.

### Actual behavior

`HandlerInvoker` logs an error and the message is never processed. The handler silently fails on every message.

### Root cause

In [`HandlerInvoker.cs`](https://github.com/aws/aws-dotnet-messaging/blob/main/src/AWS.Messaging/Services/HandlerInvoker.cs), the method lookup uses:

```csharp
subscriberMapping.HandlerType.GetMethod(
nameof(IMessageHandler.HandleAsync),
new Type[] { messageEnvelope.GetType(), typeof(CancellationToken) });
```

`Type.GetMethod(string, Type[])` with no `BindingFlags` only searches `Public | Instance` members. Explicit interface implementations are non-public on the implementing type — they're only accessible via the interface.

### Suggested fix

Since the framework already knows the interface type (`IMessageHandler`), it could invoke through the interface directly instead of using reflection on the concrete type:

```csharp
var handler = (IMessageHandler)scope.ServiceProvider.GetRequiredService(subscriberMapping.HandlerType);
await handler.HandleAsync(messageEnvelope, token);
```

This would make the reflection lookup unnecessary and support both implicit and explicit implementations.

### Workaround

Change from explicit to implicit (public) implementation:

```csharp
// Before (broken)
async Task IMessageHandler.HandleAsync(...)

// After (works)
public async Task HandleAsync(...)
```

### Additional context

- This is not documented anywhere — all samples use public implicit implementation, but nothing warns against explicit implementation
- The error message ("Unable to resolve a compatible HandleAsync method") is misleading — the handler IS resolved from DI, the method just isn't found via reflection
- Related to #166, which fixed the handler resolution (`GetService` → `GetRequiredService`) but didn't address the method lookup

### Environment

- `AWS.Messaging` version: 1.2.0
- .NET 10.0

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.