Updated on
To get a random number in a range in C#, we call Random.Shared.Next(min, max). The lower bound is inclusive and the upper bound is exclusive, so Next(1, 7) rolls a die: it returns 1 to 6 and never 7.
That exclusive bound is the part worth getting right, so we’ll look at both main random classes .NET gives us, how to include the upper bound, and what to do when a range doesn’t fit in an int.
So, let’s start.
Which Classes Generate Random Numbers in C#?
There are two main classes that exist in C# to create random numbers:Â Random and RandomNumberGenerator.
Random is a pseudorandom number generator that we can initialize using the new keyword:
var random = new Random();
We have the option of providing our own seed or letting .NET choose a random seed for us:
var seed = 3; var random = new Random(seed);
Using the same seed for separate Random objects within the same environment will generate the same series of random numbers.
RandomNumberGenerator is a secure number generator, and for numbers we don’t need an instance at all: its static GetInt32 method does the work, as we’ll see in a moment.
Notice that there’s no seed to provide. The class gets its random bytes from the platform’s cryptographic generator, such as Windows’ own or OpenSSL on Linux, so nobody, us included, can reproduce its sequence. That makes it the right choice for cryptographically secure random numbers such as tokens and keys.
For a one-off number, we don’t even need our own Random instance. Since .NET 6, Random.Shared gives us one that’s safe to call from any thread. Random.Shared and the rest of the class get their own walkthrough in our guide to the Random class.
What Does Random.Next() Return Without a Range?
Once initialized we can retrieve a random number from the Random class:
var rNum = random.Next();
This returns a non-negative integer from 0 up to, but not including, int.MaxValue (2,147,483,647).
When we only need an upper bound, random.Next(200) returns a number from 0 to 199.
We call the RandomNumberGenerator class in a slightly different way:
using System.Security.Cryptography; var upperBound = 200; var rngNum = RandomNumberGenerator.GetInt32(upperBound);
The parameter specified is the exclusive upper bound, meaning that the random number will be between 0 and the specified upper bound (not including it).
Is the Upper Bound of Random.Next Inclusive?
No. Random.Next(minValue, maxValue) returns a number that can equal minValue but is smaller than maxValue unless the two are equal. Microsoft’s API reference describes maxValue as “the exclusive upper bound of the random number returned“.
So Next(1, 7) gives us 1, 2, 3, 4, 5 or 6. In our run of 100,000 calls, 7 never came up once.
Three edge cases are worth knowing. When both bounds are equal, there’s no room for a range, and Next(5, 5) simply returns 5. When the lower bound is bigger than the upper one, the call throws an ArgumentOutOfRangeException. And negative bounds are fine: Next(-3, 3) returns values from -3 to 2.
Next(maxValue) and Next() follow the same pattern with 0 as the lower bound.
Why exclusive? It lets us pass a collection’s length straight in. Next(0, items.Length) returns a valid index when items isn’t empty, with no - 1 to forget.
The figure shows Next(1, 7) above Next(1, 7 + 1).

The lower bound is in, the upper bound is out: add one to include it.
In code, we pass both bounds to the Random.Next overload:
var lowerBound = 0; var upperBound = 1001; var rNum = random.Next(lowerBound, upperBound);
That’s why the upper bound here is 1001: we want numbers from 0 to 1000.
Keep in mind that if both upper and lower bounds are the same, the lower bound number will be returned. Furthermore, this method does allow for the returning of random negative numbers if the lower bound is less than zero.
We can generate a secure random number within a range by similarly using an overloaded method that specifies the upper and lower bound of the random range:
var lowerBound = 0; var upperBound = 20001; var rngNum = RandomNumberGenerator.GetInt32(lowerBound,upperBound);
For both methods, the first parameter is inclusive while the second parameter is exclusive; meaning that the method could return the lower bound but could not return the upper bound.
One place they differ is equal bounds: Next(5, 5) returns 5, while GetInt32(5, 5) throws an ArgumentException.
How Do We Include the Upper Bound?
We add one to it. Because the upper bound is exclusive, Next(1, 6 + 1) is how we ask for a number from 1 to 6 with both ends included, and Next(1, 10 + 1) covers 1 to 10.
That works for every upper bound but one. int.MaxValue + 1 doesn’t fit in an int, so when the bound sits in a variable, C# lets the addition wrap around to int.MinValue. The call becomes Next(lowerBound, -2147483648), the lower bound is now bigger than the upper one, and Next throws an ArgumentOutOfRangeException.
The fix is to do the arithmetic in long. NextInt64 accepts the widened bound, and its result always fits back into an int because it can’t go above the original upper bound.
So for an inclusive range that may reach int.MaxValue, we call NextInt64(lowerBound, (long)upperBound + 1) and cast the result to int. For everyday ranges, + 1 is all we need.
Here’s the everyday case, a die with both ends included:
var dieRoll = Random.Shared.Next(1, 6 + 1);
The next snippet is the trap, so don’t copy it: it compiles, and it throws the moment the upper bound is int.MaxValue:
var lowerBound = int.MaxValue - 1; var upperBound = int.MaxValue; var rNum = Random.Shared.Next(lowerBound, upperBound + 1);
Running it gives us:
System.ArgumentOutOfRangeException: 'minValue' cannot be greater than maxValue. (Parameter 'minValue')
If we write the bound as a literal, Next(1, int.MaxValue + 1), the compiler stops us with error CS0220, “The operation overflows at compile time in checked mode”. With a variable, nothing warns us until it runs.
The safe version, a static method that goes inside a class, widens the bound to long before adding one:
public static int GetPseudoRandomNumberInclusive(int lowerBound, int upperBound) =>
(int)Random.Shared.NextInt64(lowerBound, (long)upperBound + 1);
With int.MaxValue - 1 and int.MaxValue as the bounds, this returns both values and never throws.
How Do We Get a Random long or double in a Range?
When the range doesn’t fit in an int, NextInt64 takes long bounds and follows the same rules: inclusive lower bound, exclusive upper bound, and the lower bound back when the two are equal. It arrived in .NET 6:
public static long GetPseudoRandomLongWithinRange(long lowerBound, long upperBound) =>
Random.Shared.NextInt64(lowerBound, upperBound);
Calling it with 5,000,000,000 and 6,000,000,000 returns a value like 5,319,659,247, well past what Next can reach.
For doubles, NextDouble() returns a value from 0.0 up to just under 1.0, and we scale it with NextDouble() * (max - min) + min. The formula and its secure counterpart have their own article on random double numbers in a range.
Here are the bounds side by side, for the calls we’ve used:
| Call | Smallest value | Largest value | Equal bounds | Since |
|---|---|---|---|---|
Next() | 0 | int.MaxValue - 1 | n/a | .NET Framework |
Next(max) | 0 | max - 1 | Next(0) returns 0 | .NET Framework |
Next(min, max) | min | max - 1 | returns min | .NET Framework |
NextInt64(min, max) | min | max - 1 | returns min | .NET 6 |
NextDouble() | 0.0 | just under 1.0 | n/a | .NET Framework |
If we only remember one row, it’s the third. Every other method on the list shares its inclusive lower bound and exclusive upper bound.
Conclusion
In conclusion, the two main ways to generate random numbers in C# are the Random and RandomNumberGenerator classes. These pseudo-random and secure random generators give us the flexibility to pick the one that best fits our current need.
Whichever we pick, the range rule is the same: the lower bound is in and the upper bound is out. So we add one to include it, and with Random we switch to NextInt64 when that one would overflow.
We hope that you will be able to take this knowledge and use it effectively in the near future!
Tested with .NET 10.0.10 and MSTest 4.5.1.
