Use Random.Shared.Next(min, max + 1) (.NET 6+). The second argument is exclusive, so the + 1 is what lets max itself come up: Random.Shared.Next(1, 7) rolls a die from 1 to 6. For numbers that repeat on every run, create one new Random(42) and reuse it, and for passwords or tokens use RandomNumberGenerator.
Random.Next(minValue, maxValue) includes minValue and excludes maxValue. That one rule is behind dice that never roll a 6. The other classic bug is a new Random() inside a loop that hands back the same number over and over, which is why the modern answer is a single shared instance: Random.Shared, added in .NET 6. Below are integers, doubles and longs in a range, shuffling a list, random strings, and the cryptographic generator for anything secret. Random output changes on every run, so the examples either use a fixed seed or print checks such as allInRange: True instead of the numbers. Each one runs on this page: hit Run, then edit the code and run it again.
1Random.Shared.Next(min, max + 1)Recommended
Random.Shared (.NET 6+) is a ready-made, thread-safe Random, so there is nothing to create, store or lock. Next(minValue, maxValue) returns a number from minValue up to, but not including, maxValue. For an inclusive range, pass max + 1. Next(maxValue) is the same thing starting at 0, which is exactly what you want for a random index.
Output
Prints roll is between 1 and 6: True and values seen: 1, 2, 3, 4, 5, 6: over 10,000 draws every face shows up. Drop the + 1 and the result is without + 1: 1, 2, 3, 4, 5. That is the off-by-one behind the Stack Overflow question, and it fails silently. Next(10) and ranges that cross zero behave the same way (0..9: True, -10..10: True). Equal bounds are allowed and return the lower one (Next(5, 5) = 5), but reversed bounds throw ArgumentOutOfRangeException: 'minValue' cannot be greater than maxValue. (Parameter 'minValue'). On .NET Framework or anything older than .NET 6, keep one Random in a field and reuse it (one per thread if several threads draw numbers); the next section shows why.
2One Random, seeds, and the "same number every time" bug
Create a Random with a seed when you need the same numbers on every run: tests, simulations and procedurally generated levels. The catch is that the seed decides the whole sequence, so a new Random(seed) created inside a loop starts over each time and returns the same first value forever. On .NET Framework the same happened with plain new Random(), because the default seed came from the system clock and instances created within a few milliseconds of each other got the same seed. .NET Core and .NET 5+ seed each unseeded instance randomly, but the fix is the same everywhere: create one generator and reuse it.
Output
Both seeded lines print 17 11 11 15 11 12 17 15, on this run and every other. The loop that builds a new Random(42) per number prints 17 17 17 17 17 17 17 17: that is the bug from the Stack Overflow question, with the clock seed swapped for a fixed one so you can see it. Unseeded instances on modern .NET are independent (distinct values from 5 new Random(): 5). A single Random instance is not thread-safe, so the last block uses Random.Shared from Parallel.For and gets parallel rolls: 60000, faces hit: 6. The seeded values depend on the seed and on .NET's algorithm; string.Join is covered in join a list into a string.
3Doubles, floats and longs in a range
NextDouble() returns a double from 0.0 (inclusive) to 1.0 (exclusive). There is no range overload, so scale it: min + (max - min) * random.NextDouble(). .NET 6 added NextSingle() for float and NextInt64(min, max) for ranges that don't fit in an int, with the same exclusive upper bound.
Output
The seeded prices are 8.34053232955771 -> 8.34, 5.704536491867405 -> 5.70 and 5.627591447265629 -> 5.63, followed by NextSingle: 0.52276427 and NextInt64: 9343097403. The :F2 format only rounds for display; the variable keeps every digit. Plain Next() returns 0 up to int.MaxValue - 1 (Next() never negative: True), so the Math.Abs calls you see in old code are not needed. The unseeded doubles print allInRange: True.
4Shuffle a list and pick random items
.NET 8 added Random.Shuffle, which shuffles an array or Span<T> in place with Fisher-Yates. It has no List<T> overload, but CollectionsMarshal.AsSpan(list) exposes the list's backing array as a span. On older versions, write the Fisher-Yates loop yourself. .NET 10 also adds a LINQ Shuffle() that returns a shuffled copy. To pick items, index with Next(count), or use GetItems (.NET 8+) for several picks that may repeat.
Output
With new Random(42) the array becomes [7, 3, 4, 1, 6, 2, 9, 5, 8, 10], the list [Margaret, Grace, Linus, Ken, Ada] and the hand-written loop gives [5, 2, 4, 8, 3, 7, 1, 6]. LINQ Shuffle() can't take a seed, so it prints checks: LINQ Shuffle keeps every item: True, source untouched: True. The picks with new Random(7) are one: green, repeats: yellow, blue, red, green, blue and unique: red, yellow. You will also meet list.OrderBy(_ => random.Next()); it works, but it sorts, so it is slower than a shuffle. Don't modify a List<T>'s size while you hold its span.
5Random alphanumeric strings and secure tokens
A random string is a random pick of characters from an alphabet. .NET 10 has Random.GetString(alphabet, length) for that; on .NET 8 and 9, GetItems returns a char[]; older code uses the LINQ one-liner from the top Stack Overflow answer. Random is predictable, though: anyone who sees enough output can work out the rest. For passwords, reset links, session IDs and one-time codes use System.Security.Cryptography.RandomNumberGenerator, which has GetInt32(fromInclusive, toExclusive) and, since .NET 8, GetString.
Output
All three seeded lines print pIHgKQsfKvOP, which is exactly why a seeded Random must never make secrets. The RandomNumberGenerator lines change every run, so they print checks: code: 12 chars, alphanumeric: True, otp: 6 digits, numeric: True and hex token: 64 chars, base64url token: 43 chars. For tokens, encoding random bytes is simpler than picking characters: 32 bytes is 256 bits, 64 characters in hex or 43 in URL-safe Base64 (Base64Url is .NET 9+). See convert a byte array to a hex string for Convert.ToHexString.
6Which should you use?
| Method | Range | Repeatable | Best for |
|---|---|---|---|
| Random.Shared.Next(min, max + 1) | min to max (.NET 6+) | No (cannot be seeded) | Everyday random numbers, any thread |
| new Random(seed).Next(min, max + 1) | min to max | Yes, with a seed | Tests, simulations, games; one instance, one thread |
| min + (max - min) * random.NextDouble() | min to just under max | With a seeded Random | Doubles in a range |
| random.NextInt64(min, max + 1) | min to max (.NET 6+) | With a seeded Random | Ranges beyond int |
| random.Shuffle(array) | Reorders items (.NET 8+) | With a seeded Random | Shuffling, picking without repeats |
| RandomNumberGenerator.GetInt32(min, max + 1) | min to max | No (by design) | Passwords, tokens, one-time codes |
Frequently asked questions
Is the upper bound of Random.Next inclusive or exclusive?
Exclusive. Next(minValue, maxValue) returns a value from minValue up to maxValue - 1, and Next(maxValue) returns 0 to maxValue - 1. So Next(1, 6) never returns 6; for an inclusive range call Next(min, max + 1). Equal bounds return minValue, and minValue greater than maxValue throws ArgumentOutOfRangeException. Because max + 1 overflows when max is int.MaxValue, use NextInt64(min, (long)max + 1) for that edge case.
Why does my Random keep returning the same number?
Because a new Random is created for every number. On .NET Framework, new Random() was seeded from the system clock, so instances created in quick succession got the same seed and produced the same values. A new Random(42) inside a loop does the same on any version: it printed 17 17 17 17 17 17 17 17 here. Create one instance and reuse it, or call Random.Shared (.NET 6+).
Is Random thread-safe?
A Random instance is not. Microsoft documents that calling one instance from several threads at once can corrupt its state, after which it may return 0 for every call. Random.Shared (.NET 6+) is the thread-safe instance to use from parallel code. On older versions the usual pattern is one Random per thread, for example a [ThreadStatic] field or ThreadLocal<Random>.
How do I get the same random numbers every time?
Pass a seed: every new Random(42) produces the same sequence. Here eight calls to Next(0, 8) and eight calls to Next(8) on fresh new Random(42) instances both gave 5 1 1 4 1 2 5 4. Random.Shared cannot be seeded. Microsoft does not promise that the algorithm stays the same between .NET versions, so for numbers that must never change (saved games, test fixtures), store the generated values or ship your own generator.
How do I generate random numbers without duplicates?
Shuffle the candidates and take the first few. For six lottery numbers from 1 to 49, fill an array with Enumerable.Range(1, 49), call new Random(42).Shuffle(pool), and take pool[..6]: sorted, that gave 2, 8, 12, 17, 28, 33. For a few picks from a huge range, add numbers to a HashSet<int> until it has enough.
Can I use Guid.NewGuid() as a random string?
For IDs, yes. Guid.NewGuid().ToString() is 36 characters (32 hex digits plus four hyphens) and ToString("N") drops the hyphens for 32. It is a version 4 GUID, so 122 of its 128 bits are random. For secrets with a set length or alphabet, use RandomNumberGenerator.GetString or encode RandomNumberGenerator.GetBytes instead.
What is the difference between Random and RandomNumberGenerator?
Random is fast and can be seeded, which makes it right for games, simulations and tests, but its output can be predicted. RandomNumberGenerator in System.Security.Cryptography uses the operating system's cryptographic generator: it is slower, cannot be seeded, and is what passwords, tokens and keys need. Its static GetInt32(fromInclusive, toExclusive) has the same exclusive upper bound as Random.Next.