BrighterCommand / BrighterCommand/Brighter
Mapper assembly-scan registration does not filter by visibility, unlike handler registration
- Dominant language
- C#
- Stars
- 2.5k
- Forks
- 296
- Avg merge
- 1d 11h
- Merged PRs (30d)
- 21
Description
## Summary
`ServiceCollectionBrighterBuilder`'s assembly-scan mapper registration does not filter by type visibility, unlike its handler registration. This means an `internal` (or nested-private) mapper class can trigger a false-positive "mapper already registered" conflict during `AddBrighter(...).AutoFromAssemblies()`, even though it was never intended to be publicly discoverable.
## Where
`src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionBrighterBuilder.cs`
- `RegisterHandlersFromAssembly` (line 244) filters candidates with:
```csharp
where ti is { IsClass: true, IsAbstract: false, IsInterface: false } && (ti.IsPublic || ti.IsNestedPublic)
```
- `RegisterMappersFromAssemblies` (line 273) and `RegisterAsyncMappersFromAssemblies` (line 288) use the same shape of query but **omit** the `(ti.IsPublic || ti.IsNestedPublic)` clause:
```csharp
where ti is { IsClass: true, IsAbstract: false, IsInterface: false }
```
Both call `assembly.GetTypes()` (via the shared `GetLoadableTypes` helper), which returns every type in the assembly regardless of accessibility — public, internal, and nested-private alike. The handler scan then narrows that down to public/nested-public types; the mapper scans do not.
## Repro
Add an `internal` class implementing `IAmAMessageMapper` for a request type that already has a mapper, in an assembly that also calls `AddBrighter(...).AutoFromAssemblies()` anywhere:
```csharp
internal sealed class InternalDuplicateMapper : IAmAMessageMapper
{
// ... maps SomeCommand, same as the "real" public mapper
}
```
Any test/host in that assembly that runs `AddBrighter(options => { ... }).AutoFromAssemblies()` (or equivalent `MapperRegistryFromAssemblies` call) throws:
```
System.ArgumentException : A mapper for message type has already been registered.
Mappers and are in conflict
```
even though `InternalDuplicateMapper` is `internal` and was never meant to be picked up.
## Impact
- Surprising failure mode: adding an `internal` type for local/test-only use can break unrelated tests or hosts elsewhere in the same assembly that use assembly-scan registration, with no compile-time warning.
- Inconsistent with the handler scan's behaviour, which is the behaviour a reader would reasonably expect from all three scans (handlers, mappers, transforms).
- Hit in practice while implementing spec 0036 (`spec/scoped-lifetime-per-pipeline`): a test-only mapper double caused 67 unrelated test failures via assembly-scan conflict; marking it `internal` was tried as a fix and does **not** avoid the conflict, confirmed by direct reproduction.
## Proposed fix
Add the same `(ti.IsPublic || ti.IsNestedPublic)` visibility filter to `RegisterMappersFromAssemblies` and `RegisterAsyncMappersFromAssemblies` that `RegisterHandlersFromAssembly` already uses, so assembly-scan mapper registration only considers publicly visible mapper types — consistent with handler registration.
(`TransformsFromAssemblies` registers transforms by type rather than checking uniqueness per request, so it never conflicts regardless of visibility — no change needed there, but worth confirming intent while this is being looked at.)
Contributor guide
Research direction
Open src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionBrighterBuilder.cs and compare RegisterMappersFromAssemblies and RegisterAsyncMappersFromAssemblies with the visibility filter in RegisterHandlersFromAssembly. Reproduce the conflict with an internal duplicate mapper and assembly scanning, then verify that internal and nested-private mappers are ignored while public mappers retain existing registration behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 82/100