A primary constructor shortens the construction syntax, but it does not turn each parameter into a property.

public sealed class RetryPolicy(int maxAttempts)
{
    public bool CanRetry(int attempt) => attempt < maxAttempts;
}

maxAttempts is in scope throughout the type body. It is still a parameter. There is no MaxAttempts property, and neither policy.maxAttempts nor this.maxAttempts is valid member access.

Because CanRetry references the parameter, the compiler creates hidden storage for it so the value remains available after construction. That storage is an implementation detail, not a property that callers can access.

Declare a property yourself when the value should be exposed as a member of the type:

public sealed class RetryPolicy(int maxAttempts)
{
    public int MaxAttempts { get; } = maxAttempts;

    public bool CanRetry(int attempt) => attempt < MaxAttempts;
}

Once you declare a field or property, use that member inside the type as well. Continuing to reference the original parameter can make the compiler keep separate storage for both values.

Records are the exception:

public sealed record RetryPolicy(int MaxAttempts);

For a positional record, the compiler generates a public property for each primary constructor parameter. Ordinary classes and structs do not get that behavior.

For an injected dependency that only needs to be captured for internal use, referencing the primary constructor parameter directly is often enough. If the value belongs in the public API, declare a property. If you want explicit internal storage, declare a field.