diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7d888b3..b86da72 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,6 +6,7 @@ jobs: uses: FoundatioFx/Foundatio/.github/workflows/build-workflow.yml@main with: org: exceptionless + new-test-runner: true secrets: NUGET_KEY: ${{ secrets.NUGET_KEY }} FEEDZ_KEY: ${{ secrets.FEEDZ_KEY }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..9b410db --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,39 @@ +name: "Code scanning - action" + +on: + schedule: + - cron: '0 1 * * 2' + +jobs: + CodeQL-Build: + + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 + + # If this run was triggered by a pull request event, then checkout + # the head of the pull request instead of the merge commit. + - run: git checkout HEAD^2 + if: ${{ github.event_name == 'pull_request' }} + + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: csharp + + - name: Setup .NET Core + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.0.x + + - name: Build + run: dotnet build Exceptionless.RandomData.slnx --configuration Release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 0000000..232656f --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,26 @@ +name: "Copilot Setup" + +on: + workflow_dispatch: + push: + paths: + - .github/workflows/copilot-setup-steps.yml + pull_request: + paths: + - .github/workflows/copilot-setup-steps.yml + +jobs: + copilot-setup-steps: + runs-on: ubuntu-latest + + permissions: + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Setup .NET Core + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.0.x diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..9d6772f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,138 @@ +# Agent Guidelines for Exceptionless.RandomData + +You are an expert .NET engineer working on Exceptionless.RandomData, a focused utility library for generating random data useful in unit tests and data seeding. The library provides methods for generating random integers, longs, doubles, decimals, booleans, strings, words, sentences, paragraphs, dates, enums, IP addresses, versions, and coordinates. It also includes an `EnumerableExtensions.Random()` extension method to pick a random element from any collection. + +**Craftsmanship Mindset**: Every line of code should be intentional, readable, and maintainable. Write code you'd be proud to have reviewed by senior engineers. Prefer simplicity over cleverness. When in doubt, favor explicitness and clarity. + +## Repository Overview + +Exceptionless.RandomData provides random data generation utilities for .NET applications: + +- **Numeric** - `GetInt`, `GetLong`, `GetDouble`, `GetDecimal` with optional min/max ranges +- **Boolean** - `GetBool` with configurable probability (0-100%) +- **String** - `GetString`, `GetAlphaString`, `GetAlphaNumericString` with configurable length and allowed character sets +- **Text** - `GetWord`, `GetWords`, `GetTitleWords`, `GetSentence`, `GetParagraphs` with lorem ipsum-style words and optional HTML output +- **DateTime** - `GetDateTime`, `GetDateTimeOffset`, `GetTimeSpan` with optional start/end ranges +- **Enum** - `GetEnum()` to pick a random enum value (constrained to `struct, Enum`) +- **Network** - `GetIp4Address` for random IPv4 addresses +- **Version** - `GetVersion` for random version strings with optional min/max +- **Coordinate** - `GetCoordinate` for random lat/lng pairs +- **Collection** - `EnumerableExtensions.Random()` to pick a random element from any `IEnumerable` + +Design principles: **simplicity**, **thread safety** (uses `Random.Shared`), **cryptographic quality strings** (uses `RandomNumberGenerator`), **modern .NET features** (targeting net8.0/net10.0). + +## Quick Start + +```bash +# Build +dotnet build Exceptionless.RandomData.slnx + +# Test +dotnet run --project test/Exceptionless.RandomData.Tests -f net8.0 + +# Format code +dotnet format Exceptionless.RandomData.slnx +``` + +## Project Structure + +```text +src +└── Exceptionless.RandomData + └── RandomData.cs # All random data generation + EnumerableExtensions +test +└── Exceptionless.RandomData.Tests + ├── RandomDataTests.cs # Unit tests + └── Properties + └── AssemblyInfo.cs # Disables test parallelization +``` + +## Coding Standards + +### Style & Formatting + +- Follow `.editorconfig` rules (file-scoped namespaces, K&R braces) +- Follow [Microsoft C# conventions](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions) +- Use `String.`/`Int32.`/`Char.` for static method access per `.editorconfig` `dotnet_style_predefined_type_for_member_access = false` +- Run `dotnet format` to auto-format code +- Match existing file style; minimize diffs +- No code comments unless necessary—code should be self-explanatory + +### Code Quality + +- Write complete, runnable code—no placeholders, TODOs, or `// existing code...` comments +- Use modern C# features available in **net8.0/net10.0** +- **Nullable reference types** are enabled—annotate nullability correctly, don't suppress warnings without justification +- **ImplicitUsings** are enabled—don't add `using System;`, `using System.Collections.Generic;`, etc. +- Follow SOLID, DRY principles; remove unused code and parameters +- Clear, descriptive naming; prefer explicit over clever + +### Modern .NET Idioms + +- **Guard APIs**: Use `ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual()`, `ArgumentOutOfRangeException.ThrowIfLessThan()`, `ArgumentOutOfRangeException.ThrowIfGreaterThan()`, `ArgumentNullException.ThrowIfNull()`, `ArgumentException.ThrowIfNullOrEmpty()` instead of manual checks +- **`Random.Shared`**: Use `Random.Shared` instead of `new Random()` for thread-safe random number generation +- **`RandomNumberGenerator.Fill()`**: Use the static method instead of `RandomNumberGenerator.Create()` + disposal +- **Collection expressions**: Use `[...]` syntax for array initialization +- **`Span`**: Use `stackalloc` and span-based APIs to avoid allocations in hot paths +- **Expression-bodied members**: Use for single-expression methods +- **`Math.Clamp`**: Use instead of separate `Math.Min`/`Math.Max` calls +- **Generic constraints**: Use `where T : struct, Enum` instead of runtime `typeof(T).IsEnum` checks +- **Pattern matching**: Use `is null` / `is not null` instead of `== null` / `!= null` + +### Exceptions + +- Use `ArgumentOutOfRangeException.ThrowIf*` guard APIs at method entry +- Use `ArgumentException` for invalid arguments that don't fit range checks +- Include parameter names via `nameof()` where applicable +- Fail fast: throw exceptions immediately for invalid arguments + +## Making Changes + +### Before Starting + +1. **Gather context**: Read `RandomData.cs` and the test file to understand the full scope +2. **Research patterns**: Find existing usages of the code you're modifying +3. **Understand completely**: Know the problem, side effects, and edge cases before coding +4. **Plan the approach**: Choose the simplest solution that satisfies all requirements + +### While Coding + +- **Minimize diffs**: Change only what's necessary, preserve formatting and structure +- **Preserve behavior**: Don't break existing functionality or change semantics unintentionally +- **Build incrementally**: Run `dotnet build` after each logical change to catch errors early +- **Test continuously**: Run tests frequently to verify correctness +- **Match style**: Follow the patterns in surrounding code exactly + +### Validation + +Before marking work complete, verify: + +1. **Builds successfully**: `dotnet build Exceptionless.RandomData.slnx` exits with code 0 +2. **All tests pass**: `dotnet run --project test/Exceptionless.RandomData.Tests -f net8.0` shows no failures +3. **No new warnings**: Check build output for new compiler warnings (warnings are treated as errors) +4. **API compatibility**: Public API changes are intentional and backward-compatible when possible +5. **Breaking changes flagged**: Clearly identify any breaking changes for review + +## Testing + +### Framework + +- **xUnit v3** with **Microsoft Testing Platform** as the test runner +- Test parallelization is disabled via `Properties/AssemblyInfo.cs` + +### Running Tests + +```bash +# All tests, both TFMs +dotnet run --project test/Exceptionless.RandomData.Tests -f net8.0 +dotnet run --project test/Exceptionless.RandomData.Tests -f net10.0 +``` + +### Note on Namespace Conflict + +The test project uses `Exceptionless.Tests` to avoid a namespace conflict where the xUnit v3 MTP source generator creates a namespace `Exceptionless.RandomData.Tests` that shadows the `Exceptionless.RandomData` class. The test code uses `using Xunit;` and references `RandomData.*` methods directly since the `Exceptionless` namespace is accessible from within `Exceptionless.Tests`. + +## Resources + +- [README.md](README.md) - Overview and usage examples +- [NuGet Package](https://www.nuget.org/packages/Exceptionless.RandomData/) diff --git a/README.md b/README.md index 4c9e850..b4861a8 100644 --- a/README.md +++ b/README.md @@ -5,32 +5,103 @@ [![Discord](https://img.shields.io/discord/715744504891703319)](https://discord.gg/6HxgFCx) [![Donate](https://img.shields.io/badge/donorbox-donate-blue.svg)](https://donorbox.org/exceptionless?recurring=true) -Utility class to easily generate random data. This makes generating good unit test data a breeze! +A utility library for generating random data in .NET. Makes generating realistic test data a breeze. Targets **net8.0** and **net10.0**. -## Getting Started (Development) +## Getting Started -[This package](https://www.nuget.org/packages/Exceptionless.RandomData/) can be installed via the [NuGet package manager](https://docs.nuget.org/consume/Package-Manager-Dialog). If you need help, please contact us via in-app support or [open an issue](https://github.com/exceptionless/Exceptionless.RandomData/issues/new). We’re always here to help if you have any questions! +[This package](https://www.nuget.org/packages/Exceptionless.RandomData/) can be installed via the [NuGet package manager](https://docs.nuget.org/consume/Package-Manager-Dialog). If you need help, please contact us via in-app support or [open an issue](https://github.com/exceptionless/Exceptionless.RandomData/issues/new). We're always here to help if you have any questions! -1. You will need to have [Visual Studio Code](https://code.visualstudio.com/) installed. -2. Open the root folder. +``` +dotnet add package Exceptionless.RandomData +``` + +## Usage + +All methods are on the static `RandomData` class in the `Exceptionless` namespace. + +### Numbers + +```csharp +using Exceptionless; + +int value = RandomData.GetInt(1, 100); +long big = RandomData.GetLong(0, 1_000_000); +double d = RandomData.GetDouble(0.0, 1.0); +decimal m = RandomData.GetDecimal(1, 500); +``` + +### Booleans + +```csharp +using Exceptionless; + +bool coin = RandomData.GetBool(); +bool likely = RandomData.GetBool(chance: 80); // 80% chance of true +``` + +### Strings + +```csharp +using Exceptionless; + +string random = RandomData.GetString(minLength: 5, maxLength: 20); +string alpha = RandomData.GetAlphaString(10, 10); +string alphaNum = RandomData.GetAlphaNumericString(8, 16); +``` -## Using RandomData +### Words, Sentences, and Paragraphs -Below is a small sample of what you can do, so check it out! +```csharp +using Exceptionless; + +string word = RandomData.GetWord(); +string title = RandomData.GetTitleWords(minWords: 3, maxWords: 6); +string sentence = RandomData.GetSentence(minWords: 5, maxWords: 15); +string text = RandomData.GetParagraphs(count: 2, minSentences: 3, maxSentences: 10); +string html = RandomData.GetParagraphs(count: 2, html: true); +``` + +### Dates and Times + +```csharp +using Exceptionless; + +DateTime date = RandomData.GetDateTime(); +DateTime recent = RandomData.GetDateTime(start: DateTime.UtcNow.AddDays(-30), end: DateTime.UtcNow); +DateTimeOffset dto = RandomData.GetDateTimeOffset(); +TimeSpan span = RandomData.GetTimeSpan(min: TimeSpan.FromMinutes(1), max: TimeSpan.FromHours(2)); +``` + +### Enums ```csharp -private int[] _numbers = new[] { 1, 2, 3, 4, 5 }; +using Exceptionless; -private enum _days { - Monday, - Tuesday -} +DayOfWeek day = RandomData.GetEnum(); +``` + +### Network and Versioning + +```csharp +using Exceptionless; + +string ip = RandomData.GetIp4Address(); // e.g. "192.168.4.12" +string coord = RandomData.GetCoordinate(); // e.g. "45.123,-90.456" +string version = RandomData.GetVersion("1.0", "5.0"); +``` + +### Pick Random from Collection + +The `Random()` extension method picks a random element from any `IEnumerable`: + +```csharp +using Exceptionless; -int value = RandomData.GetInt(1, 5); -// or -value = _numbers.Random(); +int[] numbers = [1, 2, 3, 4, 5]; +int picked = numbers.Random(); -var day = RandomData.GetEnum<_days>(); +string[] names = ["Alice", "Bob", "Charlie"]; +string? name = names.Random(); ``` ## Thanks to all the people who have contributed diff --git a/build/common.props b/build/common.props index cfba82c..fee0004 100644 --- a/build/common.props +++ b/build/common.props @@ -1,7 +1,9 @@ - netstandard2.0 + net8.0;net10.0 + enable + enable Exceptionless RandomData Generator Exceptionless RandomData Generator https://github.com/exceptionless/Exceptionless.RandomData @@ -10,9 +12,9 @@ true v - Copyright (c) 2025 Exceptionless. All rights reserved. + Copyright © $([System.DateTime]::Now.ToString(yyyy)) Exceptionless. All rights reserved. Exceptionless - $(NoWarn);CS1591;NU1701 + $(NoWarn);CS1591 true latest true @@ -31,9 +33,9 @@ - - - + + + diff --git a/global.json b/global.json new file mode 100644 index 0000000..3140116 --- /dev/null +++ b/global.json @@ -0,0 +1,5 @@ +{ + "test": { + "runner": "Microsoft.Testing.Platform" + } +} diff --git a/src/Exceptionless.RandomData/RandomData.cs b/src/Exceptionless.RandomData/RandomData.cs index 21dfa5f..13685ff 100644 --- a/src/Exceptionless.RandomData/RandomData.cs +++ b/src/Exceptionless.RandomData/RandomData.cs @@ -1,352 +1,394 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; using System.Security.Cryptography; using System.Text; -namespace Exceptionless { - public static class RandomData { - static RandomData() { - Instance = new Random(Environment.TickCount); - } +namespace Exceptionless; - public static Random Instance { get; } +/// Generates random data for use in unit tests and data seeding. +public static class RandomData { + /// Gets the shared instance. Thread-safe. + public static Random Instance => Random.Shared; - public static int GetInt(int min, int max) { - if (min == max) - return min; + /// Returns a random integer in the inclusive range [, ]. + /// Returns immediately when both values are equal. + /// is greater than . + public static int GetInt(int min, int max) { + if (min == max) + return min; - if (min >= max) - throw new Exception("Min value must be less than max value."); + ArgumentOutOfRangeException.ThrowIfGreaterThan(min, max); - return Instance.Next(min, max + 1); - } + return Random.Shared.Next(min, max + 1); + } - public static string GetVersion(string min, string max) { - if (String.IsNullOrEmpty(min)) - min = "0.0.0.0"; - if (String.IsNullOrEmpty(max)) - max = "25.100.9999.9999"; - - Version minVersion, maxVersion; - if (!Version.TryParse(min, out minVersion)) - minVersion = new Version(0, 0, 0, 0); - if (!Version.TryParse(max, out maxVersion)) - maxVersion = new Version(25, 100, 9999, 9999); - - minVersion = new Version( - minVersion.Major != -1 ? minVersion.Major : 0, - minVersion.Minor != -1 ? minVersion.Minor : 0, - minVersion.Build != -1 ? minVersion.Build : 0, - minVersion.Revision != -1 ? minVersion.Revision : 0); - - maxVersion = new Version( - maxVersion.Major != -1 ? maxVersion.Major : 0, - maxVersion.Minor != -1 ? maxVersion.Minor : 0, - maxVersion.Build != -1 ? maxVersion.Build : 0, - maxVersion.Revision != -1 ? maxVersion.Revision : 0); - - var major = GetInt(minVersion.Major, maxVersion.Major); - var minor = GetInt(minVersion.Minor, major == maxVersion.Major ? maxVersion.Minor : 100); - var build = GetInt(minVersion.Build, minor == maxVersion.Minor ? maxVersion.Build : 9999); - var revision = GetInt(minVersion.Revision, build == maxVersion.Build ? maxVersion.Revision : 9999); - - return new Version(major, minor, build, revision).ToString(); - } + /// Returns a random integer across the full range. + public static int GetInt() => GetInt(Int32.MinValue, Int32.MaxValue); + + /// + /// Returns a random version string between and . + /// Defaults to a range of "0.0.0.0" – "25.100.9999.9999" when either bound is null or empty. + /// + public static string GetVersion(string? min, string? max) { + if (String.IsNullOrEmpty(min)) + min = "0.0.0.0"; + if (String.IsNullOrEmpty(max)) + max = "25.100.9999.9999"; + + if (!Version.TryParse(min, out var minVersion)) + minVersion = new Version(0, 0, 0, 0); + if (!Version.TryParse(max, out var maxVersion)) + maxVersion = new Version(25, 100, 9999, 9999); + + minVersion = new Version( + minVersion.Major != -1 ? minVersion.Major : 0, + minVersion.Minor != -1 ? minVersion.Minor : 0, + minVersion.Build != -1 ? minVersion.Build : 0, + minVersion.Revision != -1 ? minVersion.Revision : 0); + + maxVersion = new Version( + maxVersion.Major != -1 ? maxVersion.Major : 0, + maxVersion.Minor != -1 ? maxVersion.Minor : 0, + maxVersion.Build != -1 ? maxVersion.Build : 0, + maxVersion.Revision != -1 ? maxVersion.Revision : 0); + + var major = GetInt(minVersion.Major, maxVersion.Major); + var minor = GetInt(minVersion.Minor, major == maxVersion.Major ? maxVersion.Minor : 100); + var build = GetInt(minVersion.Build, minor == maxVersion.Minor ? maxVersion.Build : 9999); + var revision = GetInt(minVersion.Revision, build == maxVersion.Build ? maxVersion.Revision : 9999); + + return new Version(major, minor, build, revision).ToString(); + } - public static int GetInt() { - return GetInt(Int32.MinValue, Int32.MaxValue); - } + /// Returns a random in the inclusive range [, ]. + /// Returns immediately when both values are equal. + /// is greater than . + public static long GetLong(long min, long max) { + if (min == max) + return min; - public static long GetLong(long min, long max) { - if (min == max) - return min; + ArgumentOutOfRangeException.ThrowIfGreaterThan(min, max); - if (min >= max) - throw new Exception("Min value must be less than max value."); + var buf = new byte[8]; + Random.Shared.NextBytes(buf); + long longRand = BitConverter.ToInt64(buf, 0); - var buf = new byte[8]; - Instance.NextBytes(buf); - long longRand = BitConverter.ToInt64(buf, 0); + return (Math.Abs(longRand % (max - min)) + min); + } - return (Math.Abs(longRand % (max - min)) + min); - } + /// Returns a random across the full range. + public static long GetLong() => GetLong(Int64.MinValue, Int64.MaxValue); - public static long GetLong() { - return GetLong(Int64.MinValue, Int64.MaxValue); - } + /// Returns a random latitude/longitude coordinate string in the form "lat,lng". + public static string GetCoordinate() => $"{GetDouble(-90.0, 90.0)},{GetDouble(-180.0, 180.0)}"; - public static string GetCoordinate() { - return GetDouble(-90.0, 90.0) + "," + GetDouble(-180.0, 180.0); - } + /// + /// Returns a random between and . + /// Defaults to and when not specified. + /// + /// Returns immediately when both values are equal. + /// is greater than . + public static DateTime GetDateTime(DateTime? start = null, DateTime? end = null) { + if (start.HasValue && end.HasValue && start.Value == end.Value) + return start.Value; - public static DateTime GetDateTime(DateTime? start = null, DateTime? end = null) { - if (start.HasValue && end.HasValue && start.Value == end.Value) - return start.Value; - - if (start.HasValue && end.HasValue && start.Value >= end.Value) - throw new Exception("Start date must be less than end date."); + if (start.HasValue && end.HasValue) + ArgumentOutOfRangeException.ThrowIfGreaterThan(start.Value, end.Value, nameof(start)); - start = start ?? DateTime.MinValue; - end = end ?? DateTime.MaxValue; + start ??= DateTime.MinValue; + end ??= DateTime.MaxValue; - TimeSpan timeSpan = end.Value - start.Value; - var newSpan = new TimeSpan(GetLong(0, timeSpan.Ticks)); + TimeSpan timeSpan = end.Value - start.Value; + var newSpan = new TimeSpan(GetLong(0, timeSpan.Ticks)); - return start.Value + newSpan; - } + return start.Value + newSpan; + } - public static DateTimeOffset GetDateTimeOffset(DateTimeOffset? start = null, DateTimeOffset? end = null) { - if (start.HasValue && end.HasValue && start.Value >= end.Value) - throw new Exception("Start date must be less than end date."); + /// + /// Returns a random between and . + /// Defaults to and when not specified. + /// + /// is greater than or equal to . + public static DateTimeOffset GetDateTimeOffset(DateTimeOffset? start = null, DateTimeOffset? end = null) { + if (start.HasValue && end.HasValue) + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(start.Value, end.Value, nameof(start)); - start = start ?? DateTimeOffset.MinValue; - end = end ?? DateTimeOffset.MaxValue; + start ??= DateTimeOffset.MinValue; + end ??= DateTimeOffset.MaxValue; - TimeSpan timeSpan = end.Value - start.Value; - var newSpan = new TimeSpan(GetLong(0, timeSpan.Ticks)); + TimeSpan timeSpan = end.Value - start.Value; + var newSpan = new TimeSpan(GetLong(0, timeSpan.Ticks)); - return start.Value + newSpan; - } + return start.Value + newSpan; + } - public static TimeSpan GetTimeSpan(TimeSpan? min = null, TimeSpan? max = null) { - if (min.HasValue && max.HasValue && min.Value == max.Value) - return min.Value; + /// + /// Returns a random between and . + /// Defaults to and when not specified. + /// + /// Returns immediately when both values are equal. + /// is greater than . + public static TimeSpan GetTimeSpan(TimeSpan? min = null, TimeSpan? max = null) { + if (min.HasValue && max.HasValue && min.Value == max.Value) + return min.Value; - if (min.HasValue && max.HasValue && min.Value >= max.Value) - throw new Exception("Min span must be less than max span."); + if (min.HasValue && max.HasValue) + ArgumentOutOfRangeException.ThrowIfGreaterThan(min.Value, max.Value, nameof(min)); - min = min ?? TimeSpan.Zero; - max = max ?? TimeSpan.MaxValue; + min ??= TimeSpan.Zero; + max ??= TimeSpan.MaxValue; - return min.Value + new TimeSpan((long)(new TimeSpan(max.Value.Ticks - min.Value.Ticks).Ticks * Instance.NextDouble())); - } + return min.Value + new TimeSpan((long)(new TimeSpan(max.Value.Ticks - min.Value.Ticks).Ticks * Random.Shared.NextDouble())); + } - public static bool GetBool(int chance = 50) { - chance = Math.Min(chance, 100); - chance = Math.Max(chance, 0); - double c = 1 - (chance / 100.0); - return Instance.NextDouble() > c; - } + /// Returns true with the given probability percentage. + /// Probability of returning true, from 0 (never) to 100 (always). Clamped to [0, 100]. + public static bool GetBool(int chance = 50) { + chance = Math.Clamp(chance, 0, 100); + double c = 1 - (chance / 100.0); + return Random.Shared.NextDouble() > c; + } - public static double GetDouble(double? min = null, double? max = null) { - if (min.HasValue && max.HasValue && min.Value == max.Value) - return min.Value; + /// + /// Returns a random in the inclusive range [, ]. + /// Defaults to and when not specified. + /// + /// Returns immediately when both values are equal. + /// is greater than . + public static double GetDouble(double? min = null, double? max = null) { + if (min.HasValue && max.HasValue && min.Value == max.Value) + return min.Value; - if (min.HasValue && max.HasValue && min.Value >= max.Value) - throw new Exception("Min value must be less than max value."); + if (min.HasValue && max.HasValue) + ArgumentOutOfRangeException.ThrowIfGreaterThan(min.Value, max.Value, nameof(min)); - min = min ?? Double.MinValue; - max = max ?? Double.MaxValue; + min ??= Double.MinValue; + max ??= Double.MaxValue; - return Instance.NextDouble() * (max.Value - min.Value) + min.Value; - } + return Random.Shared.NextDouble() * (max.Value - min.Value) + min.Value; + } - public static decimal GetDecimal() { - return GetDecimal(GetInt(), GetInt()); - } + /// Returns a random using two random integers as bounds. + public static decimal GetDecimal() => GetDecimal(GetInt(), GetInt()); - public static decimal GetDecimal(int min, int max) { - return (decimal)GetDouble(min, max); - } + /// Returns a random in the range [, ]. + public static decimal GetDecimal(int min, int max) => (decimal)GetDouble(min, max); - public static T GetEnum() { - if (!typeof(T).GetTypeInfo().IsEnum) - throw new ArgumentException("T must be an enum type."); + /// Returns a random value from the enum type . + public static T GetEnum() where T : struct, Enum { + Array values = Enum.GetValues(typeof(T)); + return (T)values.GetValue(GetInt(0, values.Length - 1))!; + } - Array values = Enum.GetValues(typeof(T)); - return (T)values.GetValue(GetInt(0, values.Length - 1)); + /// Returns a random IPv4 address string in the form "a.b.c.d". + public static string GetIp4Address() => $"{GetInt(0, 255)}.{GetInt(0, 255)}.{GetInt(0, 255)}.{GetInt(0, 255)}"; + + private const string DEFAULT_RANDOM_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + + /// + /// Returns a random string of the specified length using as the character pool. + /// Uses a cryptographically secure source to eliminate modulo bias. + /// + /// Minimum length of the generated string. + /// Maximum length of the generated string. + /// Pool of characters to pick from. Must contain 256 or fewer distinct characters. + /// contains more than 256 distinct characters. + public static string GetString(int minLength = 5, int maxLength = 20, string allowedChars = DEFAULT_RANDOM_CHARS) { + int length = minLength != maxLength ? GetInt(minLength, maxLength) : minLength; + + const int byteSize = 0x100; + var allowedCharSet = new HashSet(allowedChars).ToArray(); + ArgumentOutOfRangeException.ThrowIfGreaterThan(allowedCharSet.Length, byteSize, nameof(allowedChars)); + + var result = new StringBuilder(); + var buf = new byte[128]; + + while (result.Length < length) { + RandomNumberGenerator.Fill(buf); + for (var i = 0; i < buf.Length && result.Length < length; ++i) { + var outOfRangeStart = byteSize - (byteSize % allowedCharSet.Length); + if (outOfRangeStart <= buf[i]) + continue; + result.Append(allowedCharSet[buf[i] % allowedCharSet.Length]); + } } - public static string GetIp4Address() { - return String.Concat(GetInt(0, 255), ".", GetInt(0, 255), ".", GetInt(0, 255), ".", GetInt(0, 255)); - } + return result.ToString(); + } - private const string DEFAULT_RANDOM_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - public static string GetString(int minLength = 5, int maxLength = 20, string allowedChars = DEFAULT_RANDOM_CHARS) { - int length = minLength != maxLength ? GetInt(minLength, maxLength) : minLength; - - const int byteSize = 0x100; - var allowedCharSet = new HashSet(allowedChars).ToArray(); - if (byteSize < allowedCharSet.Length) - throw new ArgumentException(String.Format("allowedChars may contain no more than {0} characters.", byteSize)); - - using (var rng = RandomNumberGenerator.Create()) { - var result = new StringBuilder(); - var buf = new byte[128]; - - while (result.Length < length) { - rng.GetBytes(buf); - for (var i = 0; i < buf.Length && result.Length < length; ++i) { - var outOfRangeStart = byteSize - (byteSize % allowedCharSet.Length); - if (outOfRangeStart <= buf[i]) - continue; - result.Append(allowedCharSet[buf[i] % allowedCharSet.Length]); - } - } - - return result.ToString(); - } - } + // Some characters are left out because they are hard to tell apart. + private const string DEFAULT_ALPHA_CHARS = "abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ"; - // Some characters are left out because they are hard to tell apart. - private const string DEFAULT_ALPHA_CHARS = "abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ"; - public static string GetAlphaString(int minLength = 5, int maxLength = 20) { - return GetString(minLength, maxLength, DEFAULT_ALPHA_CHARS); - } + /// Returns a random alpha string (no ambiguous characters such as l/1 or O/0). + public static string GetAlphaString(int minLength = 5, int maxLength = 20) => GetString(minLength, maxLength, DEFAULT_ALPHA_CHARS); - // Some characters are left out because they are hard to tell apart. - private const string DEFAULT_ALPHANUMERIC_CHARS = "abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789"; - public static string GetAlphaNumericString(int minLength = 5, int maxLength = 20) { - return GetString(minLength, maxLength, DEFAULT_ALPHANUMERIC_CHARS); - } - - public static string GetTitleWords(int minWords = 2, int maxWords = 10) { - return GetWords(minWords, maxWords, titleCaseAllWords: true); - } + // Some characters are left out because they are hard to tell apart. + private const string DEFAULT_ALPHANUMERIC_CHARS = "abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789"; - public static string GetWord(bool titleCase = true) { - return titleCase ? UpperCaseFirstCharacter(_words[GetInt(0, _words.Length - 1)]) : _words[GetInt(0, _words.Length - 1)]; - } + /// Returns a random alphanumeric string (no ambiguous characters such as l/1 or O/0). + public static string GetAlphaNumericString(int minLength = 5, int maxLength = 20) => GetString(minLength, maxLength, DEFAULT_ALPHANUMERIC_CHARS); - public static string GetWords(int minWords = 2, int maxWords = 10, bool titleCaseFirstWord = true, bool titleCaseAllWords = true) { - if (minWords < 2) - throw new ArgumentException("minWords must 2 or more.", "minWords"); - if (maxWords < 2) - throw new ArgumentException("maxWords must 2 or more.", "maxWords"); + /// Returns a title-cased phrase of random lorem ipsum words. + public static string GetTitleWords(int minWords = 2, int maxWords = 10) => GetWords(minWords, maxWords, titleCaseAllWords: true); - var builder = new StringBuilder(); - int numberOfWords = GetInt(minWords, maxWords); - for (int i = 1; i < numberOfWords; i++) - builder.Append(' ').Append(GetWord(titleCaseAllWords || (i == 0 && titleCaseFirstWord))); + /// Returns a single random lorem ipsum word, optionally title-cased. + public static string GetWord(bool titleCase = true) { + var word = _words[GetInt(0, _words.Length - 1)]; + return titleCase ? UpperCaseFirstCharacter(word) : word; + } - return builder.ToString().Trim(); - } + /// Returns a space-separated phrase of random lorem ipsum words. + /// Minimum number of words. Must be 2 or more. + /// Maximum number of words. Must be 2 or more. + /// Whether to title-case the first word. + /// Whether to title-case every word. + /// or is less than 2. + public static string GetWords(int minWords = 2, int maxWords = 10, bool titleCaseFirstWord = true, bool titleCaseAllWords = true) { + ArgumentOutOfRangeException.ThrowIfLessThan(minWords, 2); + ArgumentOutOfRangeException.ThrowIfLessThan(maxWords, 2); + + var builder = new StringBuilder(); + int numberOfWords = GetInt(minWords, maxWords); + for (int i = 1; i < numberOfWords; i++) + builder.Append(' ').Append(GetWord(titleCaseAllWords || (i == 0 && titleCaseFirstWord))); + + return builder.ToString().Trim(); + } - public static string GetSentence(int minWords = 5, int maxWords = 25) { - if (minWords < 3) - throw new ArgumentException("minWords must 3 or more.", "minWords"); - if (maxWords < 3) - throw new ArgumentException("maxWords must 3 or more.", "maxWords"); - - var builder = new StringBuilder(); - builder.Append(UpperCaseFirstCharacter(_words[GetInt(0, _words.Length - 1)])); - int numberOfWords = GetInt(minWords, maxWords); - for (int i = 1; i < numberOfWords; i++) - builder.Append(' ').Append(_words[GetInt(0, _words.Length - 1)]); - - builder.Append('.'); - return builder.ToString(); - } + /// Returns a random lorem ipsum sentence ending with a period. + /// Minimum number of words. Must be 3 or more. + /// Maximum number of words. Must be 3 or more. + /// or is less than 3. + public static string GetSentence(int minWords = 5, int maxWords = 25) { + ArgumentOutOfRangeException.ThrowIfLessThan(minWords, 3); + ArgumentOutOfRangeException.ThrowIfLessThan(maxWords, 3); + + var builder = new StringBuilder(); + builder.Append(UpperCaseFirstCharacter(_words[GetInt(0, _words.Length - 1)])); + int numberOfWords = GetInt(minWords, maxWords); + for (int i = 1; i < numberOfWords; i++) + builder.Append(' ').Append(_words[GetInt(0, _words.Length - 1)]); + + builder.Append('.'); + return builder.ToString(); + } - private static string UpperCaseFirstCharacter(string input) { - if (String.IsNullOrEmpty(input)) - return null; + private static string UpperCaseFirstCharacter(string input) { + if (String.IsNullOrEmpty(input)) + return input; - char[] inputChars = input.ToCharArray(); - for (int i = 0; i < inputChars.Length; ++i) { - if (inputChars[i] != ' ' && inputChars[i] != '\t') { - inputChars[i] = Char.ToUpper(inputChars[i]); - break; - } + Span chars = stackalloc char[input.Length]; + input.AsSpan().CopyTo(chars); + for (int i = 0; i < chars.Length; ++i) { + if (chars[i] != ' ' && chars[i] != '\t') { + chars[i] = Char.ToUpper(chars[i]); + break; } - - return new String(inputChars); } - public static string GetParagraphs(int count = 3, int minSentences = 3, int maxSentences = 25, int minSentenceWords = 5, int maxSentenceWords = 25, bool html = false) { - if (count < 1) - throw new ArgumentException("Count must be 1 or more.", "count"); - if (minSentences < 1) - throw new ArgumentException("minSentences must be 1 or more.", "minSentences"); + return new String(chars); + } + + /// Returns one or more paragraphs of random lorem ipsum text. + /// Number of paragraphs. Must be 1 or more. + /// Minimum sentences per paragraph. Must be 1 or more. + /// Maximum sentences per paragraph. + /// Minimum words per sentence. + /// Maximum words per sentence. + /// When true, wraps each paragraph in <p> tags. + /// or is less than 1. + public static string GetParagraphs(int count = 3, int minSentences = 3, int maxSentences = 25, int minSentenceWords = 5, int maxSentenceWords = 25, bool html = false) { + ArgumentOutOfRangeException.ThrowIfLessThan(count, 1); + ArgumentOutOfRangeException.ThrowIfLessThan(minSentences, 1); - var builder = new StringBuilder(); - if (html) - builder.Append("

"); + var builder = new StringBuilder(); + if (html) + builder.Append("

"); - builder.Append("Lorem ipsum dolor sit amet. "); - int sentenceCount = GetInt(minSentences, maxSentences) - 1; + builder.Append("Lorem ipsum dolor sit amet. "); + int sentenceCount = GetInt(minSentences, maxSentences) - 1; - for (int i = 0; i < sentenceCount; i++) - builder.Append(GetSentence(minSentenceWords, maxSentenceWords)).Append(" "); + for (int i = 0; i < sentenceCount; i++) + builder.Append(GetSentence(minSentenceWords, maxSentenceWords)).Append(' '); - if (html) - builder.Append("

"); + if (html) + builder.Append("

"); - for (int i = 1; i < count; i++) { - if (html) - builder.Append("

"); - for (int x = 0; x < sentenceCount; x++) - builder.Append(GetSentence(minSentenceWords, maxSentenceWords)).Append(" "); - - if (html) - builder.Append("

"); - else - builder.Append(Environment.NewLine).Append(Environment.NewLine); - } + for (int i = 1; i < count; i++) { + if (html) + builder.Append("

"); + for (int x = 0; x < sentenceCount; x++) + builder.Append(GetSentence(minSentenceWords, maxSentenceWords)).Append(' '); - return builder.ToString(); + if (html) + builder.Append("

"); + else + builder.Append(Environment.NewLine).Append(Environment.NewLine); } - private static string[] _words = { "consetetur", "sadipscing", "elitr", "sed", "diam", "nonumy", "eirmod", - "tempor", "invidunt", "ut", "labore", "et", "dolore", "magna", "aliquyam", "erat", "sed", "diam", "voluptua", - "at", "vero", "eos", "et", "accusam", "et", "justo", "duo", "dolores", "et", "ea", "rebum", "stet", "clita", - "kasd", "gubergren", "no", "sea", "takimata", "sanctus", "est", "lorem", "ipsum", "dolor", "sit", "amet", - "lorem", "ipsum", "dolor", "sit", "amet", "consetetur", "sadipscing", "elitr", "sed", "diam", "nonumy", "eirmod", - "tempor", "invidunt", "ut", "labore", "et", "dolore", "magna", "aliquyam", "erat", "sed", "diam", "voluptua", - "at", "vero", "eos", "et", "accusam", "et", "justo", "duo", "dolores", "et", "ea", "rebum", "stet", "clita", - "kasd", "gubergren", "no", "sea", "takimata", "sanctus", "est", "lorem", "ipsum", "dolor", "sit", "amet", - "lorem", "ipsum", "dolor", "sit", "amet", "consetetur", "sadipscing", "elitr", "sed", "diam", "nonumy", "eirmod", - "tempor", "invidunt", "ut", "labore", "et", "dolore", "magna", "aliquyam", "erat", "sed", "diam", "voluptua", - "at", "vero", "eos", "et", "accusam", "et", "justo", "duo", "dolores", "et", "ea", "rebum", "stet", "clita", - "kasd", "gubergren", "no", "sea", "takimata", "sanctus", "est", "lorem", "ipsum", "dolor", "sit", "amet", "duis", - "autem", "vel", "eum", "iriure", "dolor", "in", "hendrerit", "in", "vulputate", "velit", "esse", "molestie", - "consequat", "vel", "illum", "dolore", "eu", "feugiat", "nulla", "facilisis", "at", "vero", "eros", "et", - "accumsan", "et", "iusto", "odio", "dignissim", "qui", "blandit", "praesent", "luptatum", "zzril", "delenit", - "augue", "duis", "dolore", "te", "feugait", "nulla", "facilisi", "lorem", "ipsum", "dolor", "sit", "amet", - "consectetuer", "adipiscing", "elit", "sed", "diam", "nonummy", "nibh", "euismod", "tincidunt", "ut", "laoreet", - "dolore", "magna", "aliquam", "erat", "volutpat", "ut", "wisi", "enim", "ad", "minim", "veniam", "quis", - "nostrud", "exerci", "tation", "ullamcorper", "suscipit", "lobortis", "nisl", "ut", "aliquip", "ex", "ea", - "commodo", "consequat", "duis", "autem", "vel", "eum", "iriure", "dolor", "in", "hendrerit", "in", "vulputate", - "velit", "esse", "molestie", "consequat", "vel", "illum", "dolore", "eu", "feugiat", "nulla", "facilisis", "at", - "vero", "eros", "et", "accumsan", "et", "iusto", "odio", "dignissim", "qui", "blandit", "praesent", "luptatum", - "zzril", "delenit", "augue", "duis", "dolore", "te", "feugait", "nulla", "facilisi", "nam", "liber", "tempor", - "cum", "soluta", "nobis", "eleifend", "option", "congue", "nihil", "imperdiet", "doming", "id", "quod", "mazim", - "placerat", "facer", "possim", "assum", "lorem", "ipsum", "dolor", "sit", "amet", "consectetuer", "adipiscing", - "elit", "sed", "diam", "nonummy", "nibh", "euismod", "tincidunt", "ut", "laoreet", "dolore", "magna", "aliquam", - "erat", "volutpat", "ut", "wisi", "enim", "ad", "minim", "veniam", "quis", "nostrud", "exerci", "tation", - "ullamcorper", "suscipit", "lobortis", "nisl", "ut", "aliquip", "ex", "ea", "commodo", "consequat", "duis", - "autem", "vel", "eum", "iriure", "dolor", "in", "hendrerit", "in", "vulputate", "velit", "esse", "molestie", - "consequat", "vel", "illum", "dolore", "eu", "feugiat", "nulla", "facilisis", "at", "vero", "eos", "et", "accusam", - "et", "justo", "duo", "dolores", "et", "ea", "rebum", "stet", "clita", "kasd", "gubergren", "no", "sea", - "takimata", "sanctus", "est", "lorem", "ipsum", "dolor", "sit", "amet", "lorem", "ipsum", "dolor", "sit", - "amet", "consetetur", "sadipscing", "elitr", "sed", "diam", "nonumy", "eirmod", "tempor", "invidunt", "ut", - "labore", "et", "dolore", "magna", "aliquyam", "erat", "sed", "diam", "voluptua", "at", "vero", "eos", "et", - "accusam", "et", "justo", "duo", "dolores", "et", "ea", "rebum", "stet", "clita", "kasd", "gubergren", "no", - "sea", "takimata", "sanctus", "est", "lorem", "ipsum", "dolor", "sit", "amet", "lorem", "ipsum", "dolor", "sit", - "amet", "consetetur", "sadipscing", "elitr", "at", "accusam", "aliquyam", "diam", "diam", "dolore", "dolores", - "duo", "eirmod", "eos", "erat", "et", "nonumy", "sed", "tempor", "et", "et", "invidunt", "justo", "labore", - "stet", "clita", "ea", "et", "gubergren", "kasd", "magna", "no", "rebum", "sanctus", "sea", "sed", "takimata", - "ut", "vero", "voluptua", "est", "lorem", "ipsum", "dolor", "sit", "amet", "lorem", "ipsum", "dolor", "sit", - "amet", "consetetur", "sadipscing", "elitr", "sed", "diam", "nonumy", "eirmod", "tempor", "invidunt", "ut", - "labore", "et", "dolore", "magna", "aliquyam", "erat", "consetetur", "sadipscing", "elitr", "sed", "diam", - "nonumy", "eirmod", "tempor", "invidunt", "ut", "labore", "et", "dolore", "magna", "aliquyam", "erat", "sed", - "diam", "voluptua", "at", "vero", "eos", "et", "accusam", "et", "justo", "duo", "dolores", "et", "ea", - "rebum", "stet", "clita", "kasd", "gubergren", "no", "sea", "takimata", "sanctus", "est", "lorem", "ipsum" }; + return builder.ToString(); } - public static class EnumerableExtensions { - public static T Random(this IEnumerable items, T defaultValue = default(T)) { - if (items == null) - return defaultValue; + private static readonly string[] _words = [ + "consetetur", "sadipscing", "elitr", "sed", "diam", "nonumy", "eirmod", + "tempor", "invidunt", "ut", "labore", "et", "dolore", "magna", "aliquyam", "erat", "sed", "diam", "voluptua", + "at", "vero", "eos", "et", "accusam", "et", "justo", "duo", "dolores", "et", "ea", "rebum", "stet", "clita", + "kasd", "gubergren", "no", "sea", "takimata", "sanctus", "est", "lorem", "ipsum", "dolor", "sit", "amet", + "lorem", "ipsum", "dolor", "sit", "amet", "consetetur", "sadipscing", "elitr", "sed", "diam", "nonumy", "eirmod", + "tempor", "invidunt", "ut", "labore", "et", "dolore", "magna", "aliquyam", "erat", "sed", "diam", "voluptua", + "at", "vero", "eos", "et", "accusam", "et", "justo", "duo", "dolores", "et", "ea", "rebum", "stet", "clita", + "kasd", "gubergren", "no", "sea", "takimata", "sanctus", "est", "lorem", "ipsum", "dolor", "sit", "amet", + "lorem", "ipsum", "dolor", "sit", "amet", "consetetur", "sadipscing", "elitr", "sed", "diam", "nonumy", "eirmod", + "tempor", "invidunt", "ut", "labore", "et", "dolore", "magna", "aliquyam", "erat", "sed", "diam", "voluptua", + "at", "vero", "eos", "et", "accusam", "et", "justo", "duo", "dolores", "et", "ea", "rebum", "stet", "clita", + "kasd", "gubergren", "no", "sea", "takimata", "sanctus", "est", "lorem", "ipsum", "dolor", "sit", "amet", "duis", + "autem", "vel", "eum", "iriure", "dolor", "in", "hendrerit", "in", "vulputate", "velit", "esse", "molestie", + "consequat", "vel", "illum", "dolore", "eu", "feugiat", "nulla", "facilisis", "at", "vero", "eros", "et", + "accumsan", "et", "iusto", "odio", "dignissim", "qui", "blandit", "praesent", "luptatum", "zzril", "delenit", + "augue", "duis", "dolore", "te", "feugait", "nulla", "facilisi", "lorem", "ipsum", "dolor", "sit", "amet", + "consectetuer", "adipiscing", "elit", "sed", "diam", "nonummy", "nibh", "euismod", "tincidunt", "ut", "laoreet", + "dolore", "magna", "aliquam", "erat", "volutpat", "ut", "wisi", "enim", "ad", "minim", "veniam", "quis", + "nostrud", "exerci", "tation", "ullamcorper", "suscipit", "lobortis", "nisl", "ut", "aliquip", "ex", "ea", + "commodo", "consequat", "duis", "autem", "vel", "eum", "iriure", "dolor", "in", "hendrerit", "in", "vulputate", + "velit", "esse", "molestie", "consequat", "vel", "illum", "dolore", "eu", "feugiat", "nulla", "facilisis", "at", + "vero", "eros", "et", "accumsan", "et", "iusto", "odio", "dignissim", "qui", "blandit", "praesent", "luptatum", + "zzril", "delenit", "augue", "duis", "dolore", "te", "feugait", "nulla", "facilisi", "nam", "liber", "tempor", + "cum", "soluta", "nobis", "eleifend", "option", "congue", "nihil", "imperdiet", "doming", "id", "quod", "mazim", + "placerat", "facer", "possim", "assum", "lorem", "ipsum", "dolor", "sit", "amet", "consectetuer", "adipiscing", + "elit", "sed", "diam", "nonummy", "nibh", "euismod", "tincidunt", "ut", "laoreet", "dolore", "magna", "aliquam", + "erat", "volutpat", "ut", "wisi", "enim", "ad", "minim", "veniam", "quis", "nostrud", "exerci", "tation", + "ullamcorper", "suscipit", "lobortis", "nisl", "ut", "aliquip", "ex", "ea", "commodo", "consequat", "duis", + "autem", "vel", "eum", "iriure", "dolor", "in", "hendrerit", "in", "vulputate", "velit", "esse", "molestie", + "consequat", "vel", "illum", "dolore", "eu", "feugiat", "nulla", "facilisis", "at", "vero", "eos", "et", "accusam", + "et", "justo", "duo", "dolores", "et", "ea", "rebum", "stet", "clita", "kasd", "gubergren", "no", "sea", + "takimata", "sanctus", "est", "lorem", "ipsum", "dolor", "sit", "amet", "lorem", "ipsum", "dolor", "sit", + "amet", "consetetur", "sadipscing", "elitr", "sed", "diam", "nonumy", "eirmod", "tempor", "invidunt", "ut", + "labore", "et", "dolore", "magna", "aliquyam", "erat", "sed", "diam", "voluptua", "at", "vero", "eos", "et", + "accusam", "et", "justo", "duo", "dolores", "et", "ea", "rebum", "stet", "clita", "kasd", "gubergren", "no", + "sea", "takimata", "sanctus", "est", "lorem", "ipsum", "dolor", "sit", "amet", "lorem", "ipsum", "dolor", "sit", + "amet", "consetetur", "sadipscing", "elitr", "at", "accusam", "aliquyam", "diam", "diam", "dolore", "dolores", + "duo", "eirmod", "eos", "erat", "et", "nonumy", "sed", "tempor", "et", "et", "invidunt", "justo", "labore", + "stet", "clita", "ea", "et", "gubergren", "kasd", "magna", "no", "rebum", "sanctus", "sea", "sed", "takimata", + "ut", "vero", "voluptua", "est", "lorem", "ipsum", "dolor", "sit", "amet", "lorem", "ipsum", "dolor", "sit", + "amet", "consetetur", "sadipscing", "elitr", "sed", "diam", "nonumy", "eirmod", "tempor", "invidunt", "ut", + "labore", "et", "dolore", "magna", "aliquyam", "erat", "consetetur", "sadipscing", "elitr", "sed", "diam", + "nonumy", "eirmod", "tempor", "invidunt", "ut", "labore", "et", "dolore", "magna", "aliquyam", "erat", "sed", + "diam", "voluptua", "at", "vero", "eos", "et", "accusam", "et", "justo", "duo", "dolores", "et", "ea", + "rebum", "stet", "clita", "kasd", "gubergren", "no", "sea", "takimata", "sanctus", "est", "lorem", "ipsum" + ]; +} - var list = items.ToList(); - int count = list.Count(); - if (count == 0) - return defaultValue; +/// Extension methods for providing random element selection. +public static class EnumerableExtensions { + /// + /// Returns a random element from , or if the sequence is null or empty. + /// + public static T? Random(this IEnumerable? items, T? defaultValue = default) { + if (items is null) + return defaultValue; - return list.ElementAt(RandomData.Instance.Next(count)); - } + var list = items.ToList(); + if (list.Count == 0) + return defaultValue; + + return list.ElementAt(RandomData.Instance.Next(list.Count)); } } diff --git a/test/Exceptionless.RandomData.Tests/Exceptionless.RandomData.Tests.csproj b/test/Exceptionless.RandomData.Tests/Exceptionless.RandomData.Tests.csproj index ca1a266..a4bc421 100644 --- a/test/Exceptionless.RandomData.Tests/Exceptionless.RandomData.Tests.csproj +++ b/test/Exceptionless.RandomData.Tests/Exceptionless.RandomData.Tests.csproj @@ -1,20 +1,21 @@ - + - net8.0 + net8.0;net10.0 + Exe False + Exceptionless.Tests + $(NoWarn);CS8002 + true - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + + + - \ No newline at end of file + diff --git a/test/Exceptionless.RandomData.Tests/Properties/AssemblyInfo.cs b/test/Exceptionless.RandomData.Tests/Properties/AssemblyInfo.cs index b946c3e..00de9be 100644 --- a/test/Exceptionless.RandomData.Tests/Properties/AssemblyInfo.cs +++ b/test/Exceptionless.RandomData.Tests/Properties/AssemblyInfo.cs @@ -1 +1 @@ -[assembly: Xunit.CollectionBehaviorAttribute(DisableTestParallelization = true, MaxParallelThreads = 1)] \ No newline at end of file +[assembly: Xunit.CollectionBehavior(DisableTestParallelization = true, MaxParallelThreads = 1)] diff --git a/test/Exceptionless.RandomData.Tests/RandomDataTests.cs b/test/Exceptionless.RandomData.Tests/RandomDataTests.cs index a73bc88..499fafd 100644 --- a/test/Exceptionless.RandomData.Tests/RandomDataTests.cs +++ b/test/Exceptionless.RandomData.Tests/RandomDataTests.cs @@ -1,40 +1,40 @@ -using Xunit; - -namespace Exceptionless.Tests { - public class RandomDataTests { - [Fact] - public void RandomInt() { - int value = RandomData.GetInt(1, 5); - Assert.InRange(value, 1, 5); - - value = _numbers.Random(); - Assert.InRange(value, 1, 3); - } - - [Fact] - public void RandomDecimal() { - decimal value = RandomData.GetDecimal(1, 5); - Assert.InRange(value, 1, 5); - } - - [Fact] - public void GetEnumWithOneValueTest() { - var result = RandomData.GetEnum<_days>(); - - Assert.Equal<_days>(_days.Monday, result); - } - - [Fact] - public void GetSentencesTest() { - var result = RandomData.GetSentence(); - - Assert.False(string.IsNullOrEmpty(result)); - } - - private int[] _numbers = new[] { 1, 2, 3 }; - - private enum _days { - Monday - } +using Xunit; + +namespace Exceptionless.Tests; + +public class RandomDataTests { + [Fact] + public void RandomInt() { + int value = RandomData.GetInt(1, 5); + Assert.InRange(value, 1, 5); + + value = _numbers.Random(); + Assert.InRange(value, 1, 3); + } + + [Fact] + public void RandomDecimal() { + decimal value = RandomData.GetDecimal(1, 5); + Assert.InRange(value, 1, 5); + } + + [Fact] + public void GetEnumWithOneValueTest() { + var result = RandomData.GetEnum(); + + Assert.Equal(Days.Monday, result); + } + + [Fact] + public void GetSentencesTest() { + string result = RandomData.GetSentence(); + + Assert.False(String.IsNullOrEmpty(result)); + } + + private readonly int[] _numbers = [1, 2, 3]; + + private enum Days { + Monday } -} \ No newline at end of file +}