[API Proposal]: Add RequireNamedArgumentAttribute
- Dominant language
- C#
- Stars
- 18.3k
- Forks
- 5.6k
- PR merge metrics
- PR metrics pending
Description
## Background and motivation
This proposal supersedes [#51451](https://github.com/dotnet/runtime/issues/51451), now that the F# implementation and its cross-language design discussion provide a concrete basis for review.
Some APIs need argument names to be part of the call-site contract. The motivating F# case is generated APIs, such as SQL type providers, where changes to external metadata can reorder same-typed parameters without making existing positional calls fail to compile:
```fsharp
Query.Execute(numerator = numerator, denominator = denominator)
```
[F# RFC FS-1095](https://github.com/fsharp/fslang-design/blob/main/drafts/FS-1095-requirenamedargumentattribute.md) defines `RequireNamedArgumentAttribute` for this purpose. The [F# implementation](https://github.com/dotnet/fsharp/pull/20340) recognizes the attribute by its full metadata name, including when supplied by a polyfill, and reports FS3916 when an attributed method or constructor is used positionally. Defining the canonical attribute in `System.Runtime.CompilerServices` avoids each library declaring an equivalent polyfill and gives other .NET languages and tools one marker to recognize.
The attribute is intended for APIs where positional use is materially error-prone and parameter names are deliberately part of the source contract. Projected Objective-C or Swift APIs are another potential consumer because argument labels are part of the source API being projected. The attribute should not be applied mechanically to every member with same-typed parameters.
Related: [dotnet/runtime#51451](https://github.com/dotnet/runtime/issues/51451), [dotnet/csharplang discussion #1005](https://github.com/dotnet/csharplang/discussions/1005), [dotnet/roslyn-analyzers#1216](https://github.com/dotnet/roslyn-analyzers/issues/1216), and [StyleCopAnalyzers#2220](https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/2220).
## API Proposal
```csharp
namespace System.Runtime.CompilerServices;
[AttributeUsage(
AttributeTargets.Method |
AttributeTargets.Constructor |
AttributeTargets.Property |
AttributeTargets.Delegate |
AttributeTargets.Parameter,
AllowMultiple = false,
Inherited = false)]
public sealed class RequireNamedArgumentAttribute : Attribute
{
public RequireNamedArgumentAttribute();
}
```
Prototype: [dotnet/fsharp#20340](https://github.com/dotnet/fsharp/pull/20340)
## Compiler and analyzer behavior
F# implements the method- and constructor-wide behavior as a compiler feature gated on `--langversion:preview`:
- A call with any explicit positional argument is an error. Mixing positional and named arguments is therefore also an error.
- Omitted optional arguments and parameterless calls are allowed.
- Expanded `ParamArray` arguments are positional and produce an error; passing the array by name is allowed.
- The receiver of an extension method is not an argument that must be named.
- Constructors are covered.
- Method-group-like uses that bypass named argument syntax are rejected; callers can use a lambda that forwards with named arguments.
- Property/indexer syntax and curried F# members are not rejected where named-argument syntax is unavailable.
- The attribute is recognized by full metadata name, independent of assembly identity.
The additional targets make the contract usable without another API addition:
- Applying the attribute to a method, constructor, property, or delegate applies it to every parameter for which the language supports named-argument syntax.
- Applying the attribute to a parameter applies it only to that parameter.
- `Property` supports indexers. Ordinary property access has no argument to name.
- `Delegate` supports calls through an attributed delegate type, where the attribute cannot be placed directly on the synthesized `Invoke` method.
The current F# prototype would need follow-up support for the parameter, property, and delegate targets.
C# does not need a language feature. A corresponding .NET analyzer should recognize the same full metadata name and report a configurable recommendation when an affected argument is positional and its expression is not already self-documenting. The annotation communicates the API author's knowledge that the call can be confusing; analyzer configuration leaves the final style and severity decision with the code owner. In particular:
| Call-site argument | Diagnostic |
|---|---|
| Literal or non-trivial expression, such as `M(42)` | Yes |
| Identifier with a different name, such as `M(value)` for parameter `count` | Yes |
| Identifier matching the parameter name, such as `M(count)` | No |
| Identifier differing only by casing, such as `new R(value)` for parameter `Value` | No |
| Explicitly named argument, such as `M(count: value)` | No |
The analyzer should:
- Use the language's effective/inferred expression name when determining whether an argument is self-documenting.
- Offer a code fix that names the affected arguments.
- Exempt extension-method receivers, omitted optional arguments, parameterless calls, and contexts where named arguments are unavailable.
- Respect normal analyzer severity configuration. A warning or suggestion is more appropriate than an error because C# treats named arguments as a caller-side style choice.
Heuristic analyzers and parameter-name hints remain complementary. They can identify suspicious calls without API annotations, while this attribute communicates an explicit contract from the API author.
## API Usage
An API author can expose the same contract to F# and C# consumers:
```csharp
using System.Runtime.CompilerServices;
public static class Query
{
[RequireNamedArgument]
public static double Execute(double numerator, double denominator) =>
numerator / denominator;
}
```
```fsharp
let result = Query.Execute(numerator = value, denominator = scale)
```
```csharp
double result = Query.Execute(numerator: value, denominator: scale);
```
For example, an API can require only the ambiguous parameter to be named:
```csharp
public static void Log(
string message,
[RequireNamedArgument] string category,
LogLevel level);
Log(message, category: "Networking", level);
```
The attribute must be applied to each independently callable declaration. It does not automatically flow between interface members, implementations, virtual members, and overrides.
## Alternative Designs
- **Continue using per-library polyfills.** This already works with the F# implementation, but duplicates a compiler-recognized type across assemblies and gives analyzers no canonical BCL definition.
- **Use heuristic analysis or parameter-name hints only.** These help with likely mistakes but cannot express that an API author intentionally made names part of the contract.
- **Use IDE inlay hints.** These avoid source verbosity, but are an editor preference rather than persistent source and cannot communicate an API-specific contract to every compiler or analyzer.
- **Use records, single-case unions, units of measure, or wrapper types.** These can provide stronger type safety, but add declarations and sometimes runtime representation costs. They also do not fit generated/type-provider APIs in all cases.
- **Only support member-wide application.** This is the behavior in the current F# implementation, but it forces every argument to be named when only one or two may be ambiguous. Supporting both member and parameter targets preserves the concise member-wide form without losing precision.
- **Specify selected parameters by name in a member-level constructor**, such as `[RequireNamedArgument("category")]` or `[RequireNamedArgument(nameof(category))]`. This avoids a parameter target but is less discoverable, encodes parameter names as attribute data, and cannot use `nameof` for parameters from a method-level attribute in current C#.
- **Treat every positional argument as a violation in C#.** This follows the current F# rule but produces redundant `M(count: count)` calls. The proposed analyzer accepts a case-insensitive effective-name match as already self-documenting.
- **Require C# language support.** The C# language discussion concluded that caller-side enforcement belongs in an analyzer. A shared attribute still avoids incompatible analyzer-specific markers.
## Risks
- Compilers and tools that do not recognize the attribute will ignore it. The attribute does not provide runtime enforcement.
- F# reports an error for the currently implemented member-wide form, while the proposed C# analyzer reports a configurable recommendation and accepts matching expression names. Consumers therefore receive intentionally different enforcement across languages.
- Enabling the C# analyzer can introduce new source diagnostics for existing positional calls. It is not a binary breaking change.
- Because `Inherited` is `false`, API authors must annotate interface declarations, implementations, virtual members, and overrides separately when each surface should carry the contract.
- The contract makes parameter names more prominent for compatibility. Parameter names are already observable metadata and named-argument call sites already depend on them.
## Open questions
- **Should `Parameter`, `Property`, and `Delegate` be included now?** Proposed: yes. Attribute targets can be broadened compatibly later, but defining the intended cross-language shape before first use avoids languages and analyzers shipping incompatible semantics.
- **Should C# accept a matching expression name in place of named syntax?** Proposed: yes, using a case-insensitive comparison. This retains the readability benefit without requiring redundant `count: count`.
- **Does that C# relaxation conflict with the attribute name?** The attribute literally requires a named argument in F#. A C# analyzer treating a matching expression name as sufficient would interpret it as API intent rather than a language rule. If consistent enforcement is preferred, C# should diagnose every positional argument instead.
- **Should F# adopt parameter-level application and matching-name relaxation?** The current prototype does neither. These are F# language-design decisions, but the BCL shape should leave both possible.
- **Should member-level annotations flow to overrides or implementations?** Proposed: no implicit flow. `Inherited = false` keeps the metadata contract attached to the declaration the caller binds to.
- **Should method-group conversions produce a diagnostic?** The F# implementation rejects them. For C#, proposed: no analyzer diagnostic, because no argument expressions exist to assess and a lambda-only code fix would change code shape rather than merely add names.
## Usage in dotnet/runtime
No existing dotnet/runtime API is proposed for annotation as part of this proposal. Adoption should be considered separately and limited to APIs with a demonstrated correctness or generated-code scenario.
> [!NOTE]
> This proposal was drafted with GitHub Copilot.
Contributor guide
Research direction
Start with the API Proposal, the linked F# prototype, and the listed cross-language discussions. Review the open questions around attribute targets, analyzer behavior, and inheritance before proposing an implementation path. Done requires an agreed canonical attribute shape and clear compiler/analyzer semantics; no runtime API adoption is currently proposed.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp, fsharp
- Domain
- backend-api-design
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Needs clarification
- Newbie friendliness
- 30/100