How to Generate Random Numbers in C# Without Repeating (Within a Range)

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
VS Code running NextUpperBound.cs: C# Random.Shared.Next(1, 50) never returns 50 in 100,000 draws, while Next(1, 51) reaches 50
Next(1, 50) tops out at 49; the upper bound is exclusive

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
VS Code running RaffleShuffle.cs: C# Random.Shared.Shuffle on raffle tickets 1 to 50, and the first five shuffled tickets are five unique winners
Shuffle reorders the tickets; the first five are always unique

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')
VS Code running QuizHashSet.cs: C# HashSet loop picks 10 unique random question IDs from 1 to 5,000 and the guard throws when 11 values are asked from a 10-value range
The guard stops a request for 11 unique values out of 10

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
VS Code running SeededQuiz.cs: C# new Random(2026).Shuffle returns the same quiz question order on two runs, and seed 7 gives a different order
Same seed, same order; a different seed gives a new order

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?

SituationUseRepeats?
Draw N from a small or medium rangeRandom.Shared.Shuffle + [..n]No
A few from a huge rangeHashSet<int> loop with a count guardNo
Most of a huge rangePartial Fisher-YatesNo
Prize draws, codes, anything a user could gameRandomNumberGenerator.ShuffleNo
Repeatable testsnew Random(seed).ShuffleNo
Sampling with replacementGetItems or NextYes

For your raffle or quiz order, I’d pick Shuffle every time: one call, no repeats, fast.

These pair well with random picks:

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.