dotnet / dotnet/runtime

[API Proposal]: Add RequireNamedArgumentAttribute

Open
#132,924 3 comments 2 reactions 0 assignees View on GitHub
api-ready-for-review area-System.Runtime.CompilerServices code-analyzer
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

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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.