Generating Random Numbers in C#
Generating random numbers in C# seems like it should be a one-liner, and in many cases it is. But the language offers more than one way to produce random values, and the differences between them matter once you factor in thread safety, reproducibility, and use case. Picking the wrong approach can introduce subtle bugs in multi-threaded code or make a reported defect impossible to reproduce.
In his video "Generating Random Numbers in C#", Tim Corey walks through the classic Random class, explains why seed values exist, and introduces Random.Shared as the modern default. We'll cover each approach along with the reasoning behind it, so you can choose the right one without guessing. If you have ever wondered why there are multiple paths to something that sounds so simple, this article breaks it down.
Setting Up the Demo
[0:11 - 0:59] Tim works inside a console application running on Visual Studio 2026 (currently in preview) with .NET 10. He notes that everything demonstrated here applies equally to .NET 9 and Visual Studio 2022, so you can follow along with whatever tooling you already have installed.
The demo layout is a for loop that prints two random values side by side on each iteration. Having two outputs running in parallel makes it easier to observe whether both generators behave independently or yield matching results, a distinction that becomes important once seed values enter the picture.
The Classic Random Class
[0:59 - 3:28] The original approach to generating random integers in C# involves creating an instance of the Random class:
Random rng1 = new Random();
Random rng2 = new Random();Random rng1 = new Random();
Random rng2 = new Random();Each instance maintains its own internal state. Calling .Next(1, 101) on either one produces an integer between 1 and 100. Tim highlights a detail that trips up newcomers: the minimum value is inclusive, but the maximum is exclusive. If you want values from 1 through 100, you pass 1 and 101, not 1 and 100.
int output1 = rng1.Next(1, 101);
int output2 = rng2.Next(1, 101);int output1 = rng1.Next(1, 101);
int output2 = rng2.Next(1, 101);Running the application confirms that both instances produce different sequences. That result feels intuitive, but what happens when both instances share the same starting point tells a different story.
One important caveat about this approach: individual Random instances are not thread safe. If your application performs parallel processing and multiple threads access the same instance, the internal state can become corrupted, producing zeroes or repeated values. The safe practice is to create one instance per thread. That constraint is one of the reasons the language later introduced a better alternative.
Seed Values and Reproducible Sequences
[3:28 - 6:00] Tim then passes an explicit seed to both constructors:
Random rng1 = new Random(25);
Random rng2 = new Random(25);Random rng1 = new Random(25);
Random rng2 = new Random(25);The output changes dramatically. Both generators now produce identical sequences: 79, 16, 25, 90, 50, 41, and so on. The numbers are still individually unpredictable if you do not know the seed, but given the same starting value, the progression is deterministic.
Why would anyone want that? Tim gives a practical example. Imagine a game that generates random events throughout a session. A player reports a bug, but reproducing it seems impossible because the outcomes were randomized. If the game logs the seed used for that session, a developer can recreate the exact chain of decisions by initializing a new Random instance with the same value. The same logic applies to unit testing scenarios where you need consistent outputs to write reliable assertions against randomized behavior.
Seeded instances give you controlled randomness: a sequence that looks unpredictable but can be replayed on demand. That capability is the reason the classic Random constructor accepting a seed has not been deprecated, even though a simpler API now exists.
Random.Shared: The Modern Default
[7:36 - 9:01] Starting with .NET 6, the recommended approach for most random number generation is Random.Shared:
int output1 = Random.Shared.Next(1, 101);
int output2 = Random.Shared.Next(1, 101);int output1 = Random.Shared.Next(1, 101);
int output2 = Random.Shared.Next(1, 101);There is no instantiation involved. Random.Shared is a static, thread-safe instance managed by the runtime. You call .Next() (or any of the other methods on the Random class) and receive a value without worrying about object lifetime or concurrency.
Tim runs the demo twice to prove the point. The first execution starts with 94 and 91; the second starts with 42 and 70. Unlike a seeded instance, Random.Shared draws from a different starting state each time the process launches. You cannot set a seed, which means you cannot produce a reproducible sequence through this API. That is the tradeoff: simplicity and safety in exchange for giving up deterministic replay.
Beyond .Next(), Random.Shared exposes methods for generating doubles, filling byte arrays, and shuffling collections. For the vast majority of application code where you need a quick random value without ceremony, this single static property replaces the boilerplate of managing your own instances.
Choosing the Right Approach
[9:01 - 9:30] Tim closes with a concise decision framework. For everyday randomness (picking a value, shuffling a list, selecting a random element), Random.Shared is the right call. It requires no setup, handles concurrency, and behaves correctly across threads.
When you need a repeatable series of outputs, whether for debugging, testing, or simulation replay, create a dedicated Random instance with a known seed. Keep in mind that those instances are not safe to share across threads.
And for anything involving security (tokens, keys, password salts), neither approach is appropriate. Tim directs viewers to the cryptographic libraries in System.Security.Cryptography, which produce values that are not only random but also resistant to prediction.
Wrapping Up: Simple API, Meaningful Differences
[9:30 - 9:50] What makes this topic deceptive is how little code it takes. A single line can produce a random number through any of these approaches. The complexity is not in the syntax but in understanding which guarantees each method provides: thread safety, reproducibility, or cryptographic strength.
Conclusion
[9:50 - 10:05] To recap: Random.Shared covers most needs with zero setup and built-in thread safety. Seeded Random instances let you reproduce a specific sequence when debugging or testing requires it. Cryptographic generators belong in security-sensitive code where predictability is a vulnerability, not a feature.
The next time you reach for a random number in C#, the decision comes down to one question: do you need to replay this sequence later? If the answer is no, Random.Shared is all you need.
Example Tip: When calling Random.Shared.Next(min, max), note that max is exclusive. A range of 1 to 100 requires passing 1 and 101. This off-by-one boundary applies to seeded instances as well.
Watch full video on his YouTube Channel and gain more insights on C# fundamentals.

