Calling Random.Next in a loop is everyone’s first try to generate random numbers in C# without repeating, and it fails because nothing stops a repeat. Here’s my raffle draw for six tickets out of 50:
var rng = new Random(2026); // seeded so the run repeats
for (int i = 0; i < 6; i++)
Console.Write(rng.Next(1, 51) + " ");
// 8 15 46 50 46 4 <- ticket 46 wins twice
Quick answer: put the range in an array, call Random.Shared.Shuffle (.NET 8+) and take the first N. I ran it on .NET 10 next to a HashSet loop, a seeded shuffle for tests and RandomNumberGenerator.
Random.Next Excludes the Max Value: Get the Range Right First
Before you fix repeats, check the range. Next(min, max) includes min but never returns max, so tickets 1 to 50 need Next(1, 51). I drew 100,000 numbers with each call:
int max50 = 0, max51 = 0;
for (int i = 0; i < 100_000; i++)
{
max50 = Math.Max(max50, Random.Shared.Next(1, 50));
max51 = Math.Max(max51, Random.Shared.Next(1, 51));
}
Console.WriteLine($"Next(1, 50) highest: {max50}");
Console.WriteLine($"Next(1, 51) highest: {max51}");
Output:
Next(1, 50) highest: 49
Next(1, 51) highest: 50

The highest value from Next(1, 50) was 49. That off-by-one hides in a lot of for loops in C# that draw raffle tickets or dice.
Shuffle a Range with Random.Shared.Shuffle and Take N (Recommended)
My running sample is a made-up Lincoln Elementary PTA raffle in Portland, Oregon: tickets 1 to 50, five winners. Build the range with Enumerable.Range, shuffle it in place and slice the first five:
// Lincoln Elementary PTA raffle, Portland, OR: tickets 1-50
int[] tickets = Enumerable.Range(1, 50).ToArray();
Random.Shared.Shuffle(tickets);
int[] winners = tickets[..5];
Console.WriteLine($"Winners: {string.Join(", ", winners)}");
Console.WriteLine($"Unique: {winners.Distinct().Count()} of 5");
Output:
Winners: 19, 16, 1, 33, 20
Unique: 5 of 5

You get new winners every run (the screenshot is a second run), but always 5 of 5 unique. Shuffling only moves values, so a repeat can’t happen.
Microsoft Learn lists Shuffle as an O(n) in-place operation. The tickets[..5] slice copies a new array in C#.
Why OrderBy(_ => Random.Shared.Next()) Is Slower
Before .NET 8, the popular one-liner was sorting by a random key. It works, but it sorts, and LINQ runs the query again each time you read it:
using System.Diagnostics;
var query = Enumerable.Range(1, 50).OrderBy(_ => Random.Shared.Next()).Take(5);
Console.WriteLine($"First look: {string.Join(", ", query)}");
Console.WriteLine($"Second look: {string.Join(", ", query)}");
int[] big = Enumerable.Range(1, 1_000_000).ToArray();
var sw = Stopwatch.StartNew();
int[] sorted = big.OrderBy(_ => Random.Shared.Next()).ToArray();
Console.WriteLine($"OrderBy, 1M items: {sw.ElapsedMilliseconds} ms");
sw.Restart();
Random.Shared.Shuffle(big);
Console.WriteLine($"Shuffle, 1M items: {sw.ElapsedMilliseconds} ms");
Output:
First look: 12, 47, 9, 17, 22
Second look: 4, 33, 10, 22, 13
OrderBy, 1M items: 269 ms
Shuffle, 1M items: 10 ms
Reading the same query twice gave two different sets of winners, so call ToArray() once. On a million items the sort took 269 ms against 10 ms for Shuffle in my run.
Note: when two items get the same random key, the stable sort keeps their original order. It’s a tiny bias Shuffle doesn’t have.
HashSet<int> Loop: Pick a Few Numbers from a Huge Range
Shuffling 5,000 quiz questions to use 10 is wasteful. When N is small next to the range, draw and let a HashSet drop repeats; this is one of the few good uses of a while loop in C#:
// Pick 10 questions from a 5,000-question quiz bank
HashSet<int> PickIds(int count, int min, int maxExclusive)
{
if (count > maxExclusive - min)
throw new ArgumentOutOfRangeException(nameof(count),
$"Only {maxExclusive - min} values exist.");
var picked = new HashSet<int>();
while (picked.Count < count)
picked.Add(Random.Shared.Next(min, maxExclusive));
return picked;
}
Console.WriteLine(string.Join(", ", PickIds(10, 1, 5001)));
try { PickIds(11, 1, 11); }
catch (ArgumentOutOfRangeException ex) { Console.WriteLine(ex.Message); }
Output:
3977, 3089, 3022, 73, 4899, 4016, 2663, 109, 2587, 3428
Only 10 values exist. (Parameter 'count')

You get 10 unique IDs. The guard matters more than the loop: 11 unique values out of 10 would spin forever, so I throw. Wrap it in try-catch in C# when the count comes from user input.
Important: the loop slows down as N nears the range size, since most draws hit taken numbers. Past half the range, shuffle instead.
Partial Fisher-Yates Shuffle for Huge Ranges
A shuffle of 1 to 100,000,000 needs a 400 MB array. A partial Fisher-Yates shuffle only does N swaps, and a Dictionary in C# stores just the slots that moved:
// 5 unique IDs from 1..100,000,000 without a 400 MB array
int[] PickUnique(int min, int maxExclusive, int count)
{
var moved = new Dictionary<int, int>(); // only swapped slots
var result = new int[count];
for (int i = 0; i < count; i++)
{
int j = Random.Shared.Next(i, maxExclusive - min);
int atJ = moved.GetValueOrDefault(j, j);
moved[j] = moved.GetValueOrDefault(i, i);
result[i] = min + atJ;
}
return result;
}
int[] ids = PickUnique(1, 100_000_001, 5);
Console.WriteLine($"{string.Join(", ", ids)} (unique: {ids.Distinct().Count()})");
Output:
68850291, 19806462, 53213421, 83751850, 39228602 (unique: 5)
Five swaps, five unique IDs. Each step picks j between i and the end and swaps it with slot i, the same move as to swap two numbers in C#. No retries, even when N is close to the range size.
Random.Shared.GetItems Allows Repeats
GetItems (.NET 8+) looks like the answer, but Microsoft Learn says it picks each item with Next(Int32) on its own, so you’ll get duplicates. Seeded, ten tickets, six picks:
int[] tickets = Enumerable.Range(1, 10).ToArray();
var rng = new Random(2028);
int[] picks = rng.GetItems(tickets, 6);
Console.WriteLine($"GetItems: {string.Join(", ", picks)}");
Console.WriteLine($"Unique: {picks.Distinct().Count()} of {picks.Length}");
Output:
GetItems: 3, 9, 4, 5, 9, 9
Unique: 4 of 6
Ticket 9 came up three times. Use GetItems when you want repeats (dice, test data), never for unique winners.
How Do You Make Random Numbers Secure in C#?
Random is predictable, so a prize drawing or a sign-in code should use RandomNumberGenerator. It has its own Shuffle and GetInt32:
using System.Security.Cryptography;
// Pick-5 prize drawing from balls 1-69
int[] balls = Enumerable.Range(1, 69).ToArray();
RandomNumberGenerator.Shuffle(balls.AsSpan());
int[] draw = balls[..5];
Array.Sort(draw);
Console.WriteLine($"Draw: {string.Join(" - ", draw)}");
// 6-digit sign-in code, leading zeros kept
int code = RandomNumberGenerator.GetInt32(0, 1_000_000);
Console.WriteLine($"Code: {code:D6}");
Output:
Draw: 9 - 11 - 23 - 27 - 50
Code: 437653
Five unique balls, and D6 keeps the code’s leading zeros. GetInt32 also excludes its upper bound. I build a full one-time password flow in generate an OTP in ASP.NET.
Seeded Random for Repeatable Tests
Unit tests need the same “random” order every time. Pass a seed to new Random(seed) and the same seed gives the same shuffle:
int[] QuizOrder(int seed)
{
int[] questions = Enumerable.Range(1, 8).ToArray();
new Random(seed).Shuffle(questions);
return questions;
}
Console.WriteLine($"Seed 2026, run 1: {string.Join(", ", QuizOrder(2026))}");
Console.WriteLine($"Seed 2026, run 2: {string.Join(", ", QuizOrder(2026))}");
Console.WriteLine($"Seed 7: {string.Join(", ", QuizOrder(7))}");
Output:
Seed 2026, run 1: 2, 3, 8, 1, 4, 6, 5, 7
Seed 2026, run 2: 2, 3, 8, 1, 4, 6, 5, 7
Seed 7: 4, 8, 6, 1, 3, 2, 7, 5

Seed 2026 gave 2, 3, 8, 1, 4, 6, 5, 7 twice. Microsoft Learn warns that sequences can differ between .NET versions, so don’t store expected values across upgrades.
Never seed production draws. And don’t share one new Random() across threads; Random.Shared is the thread-safe instance.
Which Random Number Method Should You Pick?
| Situation | Use | Repeats? |
|---|---|---|
| Draw N from a small or medium range | Random.Shared.Shuffle + [..n] | No |
| A few from a huge range | HashSet<int> loop with a count guard | No |
| Most of a huge range | Partial Fisher-Yates | No |
| Prize draws, codes, anything a user could game | RandomNumberGenerator.Shuffle | No |
| Repeatable tests | new Random(seed).Shuffle | No |
| Sampling with replacement | GetItems or Next | Yes |
For your raffle or quiz order, I’d pick Shuffle every time: one call, no repeats, fast.
These pair well with random picks:
- Working with List<T> in C#
- Reverse an array in C#
- Create a GUID in C# for unique IDs
- What’s new in .NET 10
Frequently Asked Questions
How do I generate unique random numbers in a range in C#?
Put the range in an array with Enumerable.Range(min, count).ToArray(), call Random.Shared.Shuffle(array) and take the first N items. Shuffling only reorders values, so none repeat.
Is the max value inclusive in Random.Next?
No. Next(min, max) returns values from min up to max – 1. To include 50, call Next(1, 51). RandomNumberGenerator.GetInt32 works the same way.
Which .NET version has Random.Shuffle?
Random.Shuffle and GetItems were added in .NET 8 and work on .NET 9 and 10. See Random.Shuffle on Microsoft Learn. On older versions, write a Fisher-Yates loop.
Why does my HashSet loop never finish?
You asked for more unique numbers than the range holds, such as 11 from 1 to 10. Check the count against the range size before the loop and throw an ArgumentOutOfRangeException.
Should I use Random or RandomNumberGenerator?
Use Random for games, quizzes and test data. Use RandomNumberGenerator for prize draws, one-time codes and anything a user could profit from predicting, because Random isn’t cryptographically secure.
Bijay Kumar is a renowned software engineer, accomplished author, and distinguished Microsoft Most Valuable Professional (MVP) specializing in SharePoint. With a rich professional background spanning over 15 years, Bijay has established himself as an authority in the field of information technology. He possesses unparalleled expertise in multiple programming languages and technologies such as ASP.NET, ASP.NET MVC, C#.NET, and SharePoint, which has enabled him to develop innovative and cutting-edge solutions for clients across the globe. Read more…