dotnet / dotnet/aspnetcore

ProducesResponseTypeAttribute<T> / ServiceFilterAttribute<T> / TypeFilterAttribute<T> silently lose AllowMultiple across controller inheritance — they don't redeclare [AttributeUsage]

Open
#67,856 1 comment 0 reactions 0 assignees View on GitHub
area-mvc
Dominant language
C#
Stars
38.4k
Forks
10.9k
Avg merge
2d 6h
Merged PRs (30d)
290

Description

### Is there an existing issue for this?

- [x] I have searched the existing issues

### Describe the bug

The generic attribute variants derive from their non-generic bases without redeclaring `[AttributeUsage]`:

| Attribute (Mvc.Core 10.0.10) | Declares own `[AttributeUsage]`? |
|---|---|
| `ProducesResponseTypeAttribute` | yes — `AllowMultiple = true` |
| `ProducesResponseTypeAttribute` | no — inherits from base |
| `ServiceFilterAttribute` | yes — `AllowMultiple = true` |
| `ServiceFilterAttribute` | no |
| `TypeFilterAttribute` | yes — `AllowMultiple = true` |
| `TypeFilterAttribute` | no |

The C# compiler and plain attribute reflection both honor the *inherited* usage (`AllowMultiple = true` — two `[ProducesResponseType]` on one action compile and reflect fine). But the runtime's `GetCustomAttributes(inherit: true)` **hierarchy walk** resolves `AttributeUsage` only from the attribute type's own metadata and falls back to `AttributeUsageAttribute.Default` (`AllowMultiple = false`) when none is declared. MVC's application model uses exactly that call to collect controller-level attributes, so when a controller *hierarchy* declares a generic variant at more than one level, only the most-derived instance survives:

- `[ProducesResponseType]` on layered base controllers: ApiExplorer/OpenAPI silently drop the base levels — e.g. a shared `500` on an API base class vanishes from every endpoint as soon as a nearer base declares its own `403`.
- `[ServiceFilter]` / `[TypeFilter]`: worse — the base class's **filter silently stops executing** once a derived class declares one of its own. A behavioral bug, not just metadata.

The non-generic forms are unaffected because they carry their own `[AttributeUsage(..., AllowMultiple = true)]`.

### Expected Behavior

The generic and non-generic forms behave identically: instances from every level of the hierarchy are collected, consistent with the compiler accepting multiple instances and with the documented behavior of the non-generic attributes.

### Steps To Reproduce

Mechanism (single-file, `dotnet run` on .NET 10):

```csharp
using Microsoft.AspNetCore.Mvc;

// generic variant — AttributeUsage inherited from base
Console.WriteLine("generic: " + string.Join(", ",
typeof(G2).GetCustomAttributes(typeof(ProducesResponseTypeAttribute), inherit: true)
.Cast().Select(a => a.StatusCode)));
// actual: 403
// expected: 403, 401, 500

// non-generic — declares its own AttributeUsage
Console.WriteLine("non-generic: " + string.Join(", ",
typeof(N2).GetCustomAttributes(typeof(ProducesResponseTypeAttribute), inherit: true)
.Cast().Select(a => a.StatusCode)));
// prints: 403, 401, 500

[ProducesResponseType(500)] class G0;
[ProducesResponseType(401)] class G1 : G0;
[ProducesResponseType(403)] class G2 : G1;

[ProducesResponseType(typeof(ProblemDetails), 500)] class N0;
[ProducesResponseType(typeof(ProblemDetails), 401)] class N1 : N0;
[ProducesResponseType(typeof(ProblemDetails), 403)] class N2 : N1;
```

End-to-end: put the same two chains behind MVC controllers (`Base(500) ← Authorized(401) ← Restricted(403) ← concrete controller`) and inspect `/openapi/v1.json` — the generic chain yields responses `200, 400, 403`; the non-generic chain yields `200, 400, 401, 403, 500`. The same shape with `[ServiceFilter]` on two levels executes only the most-derived filter.

### Exceptions (if any)

None — the instances disappear silently.

### .NET Version

Reproduced on 10.0.10 (SDK 10.0.1xx). Present since the generic variants were introduced.

### Anything else?

The root cause is arguably the runtime's inheritance walk not honoring an *inherited* `AttributeUsage` (it disagrees with both the compiler and non-walk reflection), but that behavior is long-standing and presumably compat-frozen. Redeclaring the usage on the three generic variants fixes it completely and is binary-compatible:

```csharp
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class ProducesResponseTypeAttribute : ProducesResponseTypeAttribute
```

Any third-party attribute deriving from these (or any) attribute bases without redeclaring `[AttributeUsage]` hits the same collapse, so a docs note in the generic-attributes guidance may also be warranted.

Contributor guide

Open the contributing guide

Research direction

Start with the definitions of ProducesResponseTypeAttribute, ServiceFilterAttribute, and TypeFilterAttribute, then run the single-file dotnet reproduction described in the issue. Done means generic attributes retain all instances across controller inheritance and generic filters at each hierarchy level execute, matching the non-generic variants.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
api, backend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.