| Property | Value |
|---|---|
| Category | Constraints (JustDummies.Constraints) |
| Severity | 🔵 Info |
| Enabled by default | Yes |
A chain declares both inclusive bounds of a range separately, and the same generator names that range in a single call.
Nothing is wrong here, and nothing has to change. The two spellings behave identically — the range method is the two bounds — and declaring them separately is decomposable on purpose, so a shared helper can set a floor and a call site add a ceiling. This rule exists for one reason: the range form is easy to miss, and a reader who writes the bounds separately never learns it exists. It says so where you can act on it (ADR-0077 opens in a new tab).
Noncompliant
string reference = Any.String().WithMinLength(8).WithMaxLength(20).Generate();
int quantity = Any.Int32().GreaterThanOrEqualTo(1).LessThanOrEqualTo(50).Generate();
Compliant
string reference = Any.String().WithLengthBetween(8, 20).Generate();
int quantity = Any.Int32().Between(1, 50).Generate();
Beyond being shorter, the range form reads closer to how the rule is usually stated out loud — “between 8 and 20 characters”, not “at least 8, at most 20” — and it carries one thing the pair does not: it records a single constraint, so a conflict raised against it later names the range you wrote rather than one of its halves.
The four vocabularies
| declared pair | reported as | generators |
|---|---|---|
WithMinLength(a) + WithMaxLength(b) | WithLengthBetween(a, b) | AnyString |
WithMinCount(a) + WithMaxCount(b) | WithCountBetween(a, b) | every collection generator |
GreaterThanOrEqualTo(a) + LessThanOrEqualTo(b) | Between(a, b) | the numeric generators and TimeSpan |
AfterOrEqualTo(a) + BeforeOrEqualTo(b) | Between(a, b) | AnyDateOnly, AnyDateTime, AnyDateTimeOffset, AnyTimeOnly |
The reported call is always (minimum, maximum), whatever order you wrote the bounds in; the diagnostic lands on whichever bound you wrote first.
Inclusive bounds only — why the strict pair is silent
Between is inclusive on both sides. GreaterThan / LessThan and After / Before are strict, so they are not the same constraint and have no exact range form:
int quantity = Any.Int32().GreaterThan(5).LessThan(10).Generate(); // silent: this is Between(6, 9)
double ratio = Any.Double().GreaterThan(1.5).LessThan(9.5).Generate(); // silent: no Between form exists at all
On an integral type the range exists but its numbers are not the ones you wrote, and reporting it would ask you to read bounds you never chose. On a floating-point type there is no next value, so there is no range form to name. Mixed pairs — GreaterThan(a).LessThanOrEqualTo(b), After(a).BeforeOrEqualTo(b) — are silent for the same reason. Inclusive plus inclusive only. Every other combination is silent.
What else it does not flag
- Bounds that are not in the same chain. Declaring them separately is a feature: a helper sets a floor, a call site adds a ceiling. Only a single fluent chain is ever paired — never a generator travelling through a local, a field, a parameter or two statements.
- A bound declared twice.
WithMinLength(8).WithMinLength(10).WithMaxLength(20)keeps only the tighter minimum, soWithLengthBetween(8, 20)would be wider than the chain draws. The pair is refused rather than guessed. - A bound reached through an alias.
NonEmpty()is a minimum length of one andPositive()a minimum of one, soAny.String().NonEmpty().WithMaxLength(20)does declare both bounds — and stays silent. Choosing the alias says something about intent the explicit bound does not. - An exact size beside the pair. A chain that also declares
WithLengthorWithCountis none of this rule’s business. - A chain already written as a range.
Why it never suggests WithLength or WithCount
A pair of equal bounds is reported as WithLengthBetween(8, 8), never as WithLength(8). They are not the same: WithLength settles the length without drawing, where a minimum and a maximum of eight still draw across a one-value range and consume a draw doing it. On a seeded run the two spellings diverge from that point on, and a replayed seed is a guarantee this library does not trade for a shorter line (ADR-0049 opens in a new tab). Every form this rule names is equivalent by construction — the range method is implemented as the two bounds it replaces — and that is the condition it is bounded by.
Why it is information, not a warning
The two-bound form is correct and is blessed by the library’s own documentation, which states that the range and the pair behave identically and that this is what keeps a range decomposable. Promoting this to a warning would have the analyzers contradict the API docs. It states a fact to weigh, never a verdict — the same posture as JD024, JD029 and JD030. Where you wrote the pair on purpose, silence it there with a [SuppressMessage] naming JustDummiesRule.JD031 — see JustDummies.DiagnosticCatalog.