A flexible .NET library for truly random selection with weighted entries. Perfect for raffles, lottery drawings, contests, and any scenario where you need fair, cryptographically-secure random selection.
The inspiration for this library came from observing bias in prize drawings where participants won multiple times while others were consistently overlooked. RandomSelection is designed to ensure fair, unbiased selection by:
- Accepting weighted entries - Give some participants higher chances than others without complex probability calculations
- Using cryptographic randomness - Utilizes
System.Security.Cryptographyfor high-quality random numbers instead of weak pseudo-random generators - Shuffling the selection pool - Randomizes the entire pool before selection to eliminate positional bias
- Supporting multiple selections - Select one or many winners in a single operation
- Type-safe and flexible - Generic
Selector<T>works with any data type
Install via NuGet:
dotnet add package RandomSelectionOr via Package Manager Console:
Install-Package RandomSelection
The Selector<T> class is the main entry point. It's generic and can work with any type of value you want to track. It maintains a pool of items and provides methods to randomly select from them.
// Works with strings
var stringSelector = new Selector<string>();
// Works with custom objects
var objectSelector = new Selector<Employee>();
// Works with any type
var intSelector = new Selector<int>();Each item can have multiple "entries" in the selection pool. This creates a weighted random selection:
- An item with 1 entry has a baseline chance
- An item with 3 entries is 3x more likely to be selected
- An item with 10 entries is 10x more likely to be selected
Real-world example: In a loyalty rewards drawing where VIP members should win 4x more often:
- Regular member: 1 entry
- Silver member: 2 entries
- Gold member: 3 entries
- Platinum member: 4 entries
Select one item from a group:
using BNolan.RandomSelection;
var selector = new Selector<string>();
selector.TryAddItem("emp001", "John Doe");
selector.TryAddItem("emp002", "Jane Smith");
selector.TryAddItem("emp003", "Mike Johnson");
// Select 1 random employee
var winners = selector.RandomSelect(1);
Console.WriteLine($"Winner: {winners[0].Value}");
// Output: Winner: Mike Johnson (example)Give some participants higher chances of winning:
var selector = new Selector<string>();
// Regular members: 1 entry each
selector.TryAddItem("jen", "Jennifer", 1);
selector.TryAddItem("michael", "Michael", 1);
selector.TryAddItem("dave", "David", 1);
// VIP member: 3 entries (3x more likely to win)
selector.TryAddItem("staci", "Staci", 3);
// Select 2 winners from the pool
var winners = selector.RandomSelect(2);
foreach (var winner in winners)
{
Console.WriteLine($"Winner: {winner.Value}");
}Work with any type of data, not just strings:
using BNolan.RandomSelection;
using BNolan.RandomSelection.Library;
public class Employee
{
public string Id { get; set; }
public string Name { get; set; }
public string Department { get; set; }
public decimal Salary { get; set; }
}
var selector = new Selector<Employee>();
var alice = new Employee { Id = "001", Name = "Alice", Department = "Engineering", Salary = 120000 };
var bob = new Employee { Id = "002", Name = "Bob", Department = "Sales", Salary = 90000 };
selector.TryAddItem("emp001", alice);
selector.TryAddItem("emp002", bob);
var winner = selector.RandomSelect(1);
Console.WriteLine($"Selected: {winner[0].Value.Name} from {winner[0].Value.Department}");For explicit control over all properties, use the Item<T> class:
using BNolan.RandomSelection.Library;
var selector = new Selector<string>();
var item1 = new Item<string>
{
UniqueId = "ticket001",
Value = "John Doe",
Entries = 2
};
var item2 = new Item<string>
{
UniqueId = "ticket002",
Value = "Jane Smith",
Entries = 1
};
selector.TryAddItem(item1);
selector.TryAddItem(item2);
var winner = selector.RandomSelect();
Console.WriteLine($"Winner: {winner[0].Value}");Add multiple items efficiently in one operation:
var employees = new List<string> { "Alice", "Bob", "Charlie", "Diana" };
var selector = new Selector<string>();
if (selector.TryAddItems(employees))
{
Console.WriteLine("All employees added successfully");
var selected = selector.RandomSelect(2);
}
else
{
Console.WriteLine("Failed to add employees");
}Adds an item with just a unique ID. The value defaults to the uniqueId, and entries default to 1.
bool success = selector.TryAddItem("participant1");| Parameter | Type | Description |
|---|---|---|
uniqueId |
T | Unique identifier (case-insensitive) |
| Returns | bool | true if added, false if ID already exists |
Adds an item with a unique ID and associated value. Entries default to 1.
bool success = selector.TryAddItem("emp001", "John Doe");
bool success = selector.TryAddItem("ticket123", myCustomObject);| Parameter | Type | Description |
|---|---|---|
uniqueId |
string | Unique identifier (case-insensitive) |
value |
T | Associated value of any type |
| Returns | bool | true if added, false if ID already exists |
Adds an item with a unique ID, value, and number of entries (weight).
// Regular member: 1 entry
selector.TryAddItem("user001", "Regular User", 1);
// VIP member: 5x more likely to be selected
selector.TryAddItem("user002", "VIP Member", 5);| Parameter | Type | Description |
|---|---|---|
uniqueId |
string | Unique identifier (case-insensitive) |
value |
T | Associated value of any type |
entries |
int | Number of entries/weight (must be > 0) |
| Returns | bool | true if added, false if ID already exists |
| Throws | ArgumentException | If entries < 1 |
Adds an Item<T> object directly for maximum control.
var item = new Item<string>
{
UniqueId = "id1",
Value = "My Value",
Entries = 3
};
if (selector.TryAddItem(item))
{
Console.WriteLine("Item added successfully");
}| Parameter | Type | Description |
|---|---|---|
item |
Item | Item object with UniqueId, Value, and Entries |
| Returns | bool | true if added, false if ID already exists |
| Throws | ArgumentNullException | If item is null or UniqueId is null/empty |
| Throws | ArgumentException | If Entries < 1 |
Adds multiple items at once. This is an all-or-nothing operation: if any item already exists, no items are added.
var employees = new List<string> { "Alice", "Bob", "Charlie" };
if (selector.TryAddItems(employees))
{
Console.WriteLine("All employees added");
}
else
{
Console.WriteLine("Failed: at least one employee already exists");
}| Parameter | Type | Description |
|---|---|---|
items |
List | Items to add |
| Returns | bool | true if all items added, false if any already exist |
| Throws | ArgumentNullException | If items list is null |
Randomly selects items from the pool. Each item can only be selected once per call.
// Select 1 winner (default)
var winner = selector.RandomSelect();
// Select 3 winners
var topThree = selector.RandomSelect(3);
foreach (var item in topThree)
{
Console.WriteLine($"ID: {item.UniqueId}, Value: {item.Value}");
}| Parameter | Type | Description |
|---|---|---|
numToSelect |
int | Number of items to select (default: 1) |
| Returns | List<Item> | List of selected Item objects |
Return Structure:
List<Item<T>>
{
new Item<T> { UniqueId = "id1", Value = yourValue, Entries = 1 },
new Item<T> { UniqueId = "id2", Value = yourValue, Entries = 3 }
}Creates the internal pool list with items repeated according to their entry count. Useful for debugging or understanding how the pool is constructed.
selector.TryAddItem("id1", "value1", 2);
selector.TryAddItem("id2", "value2", 1);
var pool = selector.GenerateList();
// Result: ["id1", "id1", "id2"]
Console.WriteLine($"Pool size: {pool.Count}");| Returns | List | List of unique IDs, each repeated by its entry count |
Shuffles a list using cryptographic randomness. Can be used independently of selection.
var original = new List<string> { "a", "b", "c", "d", "e" };
var shuffled = selector.RandomizeList(original);
// original remains unchanged
// shuffled contains a random permutation of the items| Parameter | Type | Description |
|---|---|---|
items |
List | List to shuffle |
| Returns | List | New shuffled list (original is not modified) |
Generates a random index between 0 and upperLimit (exclusive). Uses cryptographic randomness and can be used independently.
int randomIndex = selector.GenerateRandomIndex(100);
// Returns a random number from 0 to 99
int diceRoll = selector.GenerateRandomIndex(6);
// Simulates a die roll (0-5)| Parameter | Type | Description |
|---|---|---|
upperLimit |
int | Upper bound (exclusive) |
| Returns | int | Random integer from 0 to upperLimit - 1 |
| Throws | ArgumentOutOfRangeException | If upperLimit < 1 |
All add methods validate input and throw appropriate exceptions. Always handle these when accepting user input:
try
{
selector.TryAddItem((Item<string>)null); // ArgumentNullException
}
catch (ArgumentNullException ex)
{
Console.WriteLine($"Validation error: {ex.ParamName}");
}
try
{
selector.TryAddItem("id1", "value", 0); // ArgumentException
}
catch (ArgumentException ex)
{
Console.WriteLine($"Invalid argument: {ex.ParamName}");
}Common Exceptions:
ArgumentNullException- When item or UniqueId is null/emptyArgumentException- When entries < 1ArgumentOutOfRangeException- When GenerateRandomIndex called with upperLimit < 1
This library uses System.Security.Cryptography instead of System.Random for several important reasons:
| Feature | System.Random | Cryptographic RNG |
|---|---|---|
| Predictable | Yes (if seed known) | No - truly random |
| Suitable for fairness | No | Yes ? |
| Passes statistical tests | No | Yes ? |
| Seed bias | Yes | No ? |
| Reproducible | Yes (sometimes a problem) | No |
For raffles and contests where fairness matters, cryptographic randomness is essential.
- First selection: Building the pool is O(n) where n = total number of entries. This happens once per
RandomSelect()call. - Shuffling: Uses Fisher-Yates algorithm, O(n) complexity
- Cryptographic calls: Each random index requires at least one cryptographic RNG call
- Repeated selections: Reuse the same Selector instance for multiple selections to avoid rebuilding the pool
Optimization Tip: For large selections, it's more efficient to call RandomSelect(10) once rather than RandomSelect(1) ten times.
var selector = new Selector<string>();
// Add raffle participants
selector.TryAddItem("ticket001", "John Smith");
selector.TryAddItem("ticket002", "Jane Doe");
selector.TryAddItem("ticket003", "Bob Johnson");
selector.TryAddItem("ticket004", "Alice Williams");
// Draw 3 winners
var winners = selector.RandomSelect(3);
foreach (var winner in winners)
{
Console.WriteLine($"Winner: {winner.Value}");
}var selector = new Selector<string>();
// Regular participants (1x chance)
selector.TryAddItem("regular001", "Regular User 1", 1);
selector.TryAddItem("regular002", "Regular User 2", 1);
// VIP gets enhanced odds
selector.TryAddItem("vip001", "Premium User", 5);
var winner = selector.RandomSelect(1);
Console.WriteLine($"Contest winner: {winner[0].Value}");var employees = new List<(string id, string name, string role)>
{
("emp001", "Alice Johnson", "Developer"),
("emp002", "Bob Smith", "Designer"),
("emp003", "Charlie Brown", "QA"),
("emp004", "Diana Prince", "Manager")
};
var selector = new Selector<(string, string)>();
foreach (var emp in employees)
{
selector.TryAddItem(emp.id, (emp.name, emp.role));
}
var assignedEmployee = selector.RandomSelect(1)[0];
Console.WriteLine($"On-call: {assignedEmployee.Value.Item1} ({assignedEmployee.Value.Item2})");public class LotteryPlayer
{
public string Name { get; set; }
public int TicketsPurchased { get; set; }
}
var selector = new Selector<LotteryPlayer>();
selector.TryAddItem("player001", new LotteryPlayer { Name = "Alice", TicketsPurchased = 5 }, 5);
selector.TryAddItem("player002", new LotteryPlayer { Name = "Bob", TicketsPurchased = 3 }, 3);
selector.TryAddItem("player003", new LotteryPlayer { Name = "Charlie", TicketsPurchased = 10 }, 10);
// Winners weighted by ticket count
var winner = selector.RandomSelect(1);
Console.WriteLine($"Jackpot winner: {winner[0].Value.Name}");? Weighted selection - Control probability with entry counts
? Cryptographically secure - Uses System.Security.Cryptography
? Generic support - Works with any data type (T)
? Bulk operations - Add and select multiple items efficiently
? No duplicates - Each selection returns unique winners
? Comprehensive validation - Clear error messages
? Thoroughly tested - Includes statistical randomness tests
? Easy API - Intuitive methods with sensible defaults
The library includes comprehensive unit tests covering:
- Basic functionality (adding items, selection)
- Error handling and validation
- Statistical randomness verification
- Edge cases (single item, all items, duplicates)
- Distribution uniformity tests
Run tests with:
dotnet testMIT License - See repository for details