RandomSelection is designed to take in entries, with the option to give some entries a higher chance at being selected, and then return one or more selectees. It utilizes System.Security.Cryptography to ensure a higher quality random number is used for shuffling the data and selecting winners.
$ dotnet add package RandomSelectionA 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:
System.Security.Cryptography for high-quality random numbers instead of weak pseudo-random generatorsSelector<T> works with any data typeInstall via NuGet:
dotnet add package RandomSelection
Or 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:
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");
}
bool TryAddItem(T uniqueId)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 |
bool TryAddItem(string uniqueId, T value)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 |
bool TryAddItem(string uniqueId, T value, int entries)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 |
bool TryAddItem(Item<T> item)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 |
bool TryAddItems(List<T> items)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 |
List<Item<T>> RandomSelect(int numToSelect = 1)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 }
}
List<string> GenerateList()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 |
List<string> RandomizeList(List<string> items)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) |
int GenerateRandomIndex(int upperLimit)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 < 1This 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.
RandomSelect() call.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:
Run tests with:
dotnet test
MIT License - See repository for details