From e850b06913d406fc8f9d72b3f80dac8d1015d3ba Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Fri, 20 Feb 2026 14:46:51 -0600 Subject: [PATCH 1/3] Modernize library: target net8.0/net10.0, modern C# idioms, xUnit v3 - Drop netstandard2.0, target net8.0 and net10.0 LTS only - Enable nullable reference types and implicit usings - Replace static Random instance with Random.Shared (thread-safe) - Replace RandomNumberGenerator.Create() + disposal with RandomNumberGenerator.Fill() - Use ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual/ThrowIfLessThan/ThrowIfGreaterThan guard APIs everywhere instead of manual throws - Add where T : struct, Enum constraint on GetEnum() to eliminate runtime type check - Use Math.Clamp instead of separate Math.Min/Math.Max in GetBool - Rewrite UpperCaseFirstCharacter with stackalloc Span for zero-allocation - Convert to file-scoped namespace, collection expressions, expression-bodied members - Use string interpolation instead of concatenation - Add nullable annotations to GetVersion parameters and EnumerableExtensions - Upgrade build packages: SourceLink 10.0.103, AsyncFixer 2.1.0, MinVer 7.0.0 - Use dynamic copyright year expression matching project convention - Upgrade test project to xUnit v3 with Microsoft Testing Platform - Add Foundatio.Xunit.v3 13.0.0-beta3, xunit.v3.mtp-v2 3.2.2 - Add TestingPlatformDotnetTestSupport and new-test-runner: true for CI - Restore test parallelization disabling via AssemblyInfo.cs - Add codeql-analysis.yml, copilot-setup-steps.yml, global.json, AGENTS.md - Update README with comprehensive usage examples (all snippets compile clean) Co-authored-by: Cursor --- .github/workflows/build.yml | 1 + .github/workflows/codeql-analysis.yml | 39 ++ .github/workflows/copilot-setup-steps.yml | 26 + AGENTS.md | 138 +++++ README.md | 103 +++- build/common.props | 14 +- global.json | 5 + src/Exceptionless.RandomData/RandomData.cs | 533 ++++++++---------- .../Exceptionless.RandomData.Tests.csproj | 23 +- .../Properties/AssemblyInfo.cs | 2 +- .../RandomDataTests.cs | 78 +-- 11 files changed, 603 insertions(+), 359 deletions(-) create mode 100644 .github/workflows/codeql-analysis.yml create mode 100644 .github/workflows/copilot-setup-steps.yml create mode 100644 AGENTS.md create mode 100644 global.json 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..bf78c8c 100644 --- a/src/Exceptionless.RandomData/RandomData.cs +++ b/src/Exceptionless.RandomData/RandomData.cs @@ -1,352 +1,313 @@ -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; } +public static class RandomData { + public static Random Instance => Random.Shared; - public static int GetInt(int min, int max) { - if (min == max) - return min; + 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.ThrowIfGreaterThanOrEqual(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(); - } + public static int GetInt() => GetInt(Int32.MinValue, Int32.MaxValue); + + 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); - } + 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.ThrowIfGreaterThanOrEqual(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); - } + public static long GetLong() => GetLong(Int64.MinValue, Int64.MaxValue); - public static long GetLong() { - return GetLong(Int64.MinValue, Int64.MaxValue); - } + 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); - } + 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.ThrowIfGreaterThanOrEqual(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."); + 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; + 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.ThrowIfGreaterThanOrEqual(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; - } + 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; + 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.ThrowIfGreaterThanOrEqual(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()); - } + public static decimal GetDecimal() => GetDecimal(GetInt(), GetInt()); - public static decimal GetDecimal(int min, int max) { - return (decimal)GetDouble(min, max); - } + 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."); + 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)); - } + public static string GetIp4Address() => $"{GetInt(0, 255)}.{GetInt(0, 255)}.{GetInt(0, 255)}.{GetInt(0, 255)}"; - public static string GetIp4Address() { - return String.Concat(GetInt(0, 255), ".", GetInt(0, 255), ".", GetInt(0, 255), ".", GetInt(0, 255)); - } + 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; - 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(); + 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]); } } - // 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); - } + return result.ToString(); + } - // 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_ALPHA_CHARS = "abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ"; + public static string GetAlphaString(int minLength = 5, int maxLength = 20) => GetString(minLength, maxLength, DEFAULT_ALPHA_CHARS); - public static string GetWord(bool titleCase = true) { - return titleCase ? UpperCaseFirstCharacter(_words[GetInt(0, _words.Length - 1)]) : _words[GetInt(0, _words.Length - 1)]; - } + // 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) => 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"); + 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))); + public static string GetWord(bool titleCase = true) { + var word = _words[GetInt(0, _words.Length - 1)]; + return titleCase ? UpperCaseFirstCharacter(word) : word; + } - return builder.ToString().Trim(); - } + public static string GetWords(int minWords = 2, int maxWords = 10, bool titleCaseFirstWord = true, bool titleCaseAllWords = true) { + ArgumentOutOfRangeException.ThrowIfLessThan(minWords, 2); + ArgumentOutOfRangeException.ThrowIfLessThan(maxWords, 2); - 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(); - } + var builder = new StringBuilder(); + int numberOfWords = GetInt(minWords, maxWords); + for (int i = 1; i < numberOfWords; i++) + builder.Append(' ').Append(GetWord(titleCaseAllWords || (i == 0 && titleCaseFirstWord))); - private static string UpperCaseFirstCharacter(string input) { - if (String.IsNullOrEmpty(input)) - return null; + return builder.ToString().Trim(); + } - 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; - } - } + public static string GetSentence(int minWords = 5, int maxWords = 25) { + ArgumentOutOfRangeException.ThrowIfLessThan(minWords, 3); + ArgumentOutOfRangeException.ThrowIfLessThan(maxWords, 3); - return new String(inputChars); + 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 input; + + 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; + } } - 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); + } - var builder = new StringBuilder(); - if (html) - builder.Append("

"); + 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); - builder.Append("Lorem ipsum dolor sit amet. "); - int sentenceCount = GetInt(minSentences, maxSentences) - 1; + var builder = new StringBuilder(); + if (html) + builder.Append("

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

"); + for (int i = 0; i < sentenceCount; i++) + builder.Append(GetSentence(minSentenceWords, maxSentenceWords)).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); - } + 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(' '); - 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; +public static class EnumerableExtensions { + 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..5a911d3 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<_days>(); + + Assert.Equal<_days>(_days.Monday, result); + } + + [Fact] + public void GetSentencesTest() { + var result = RandomData.GetSentence(); + + Assert.False(String.IsNullOrEmpty(result)); + } + + private int[] _numbers = [1, 2, 3]; + + private enum _days { + Monday } -} \ No newline at end of file +} From 09b42eec938a62b355c9758d8e539b3e90222bb6 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Fri, 20 Feb 2026 15:03:54 -0600 Subject: [PATCH 2/3] Fix argument guards: ThrowIfGreaterThan not ThrowIfGreaterThanOrEqual when equal is allowed; add XML docs to all public API Co-authored-by: Cursor --- src/Exceptionless.RandomData/RandomData.cs | 91 ++++++++++++++++++++-- 1 file changed, 86 insertions(+), 5 deletions(-) diff --git a/src/Exceptionless.RandomData/RandomData.cs b/src/Exceptionless.RandomData/RandomData.cs index bf78c8c..13685ff 100644 --- a/src/Exceptionless.RandomData/RandomData.cs +++ b/src/Exceptionless.RandomData/RandomData.cs @@ -3,20 +3,30 @@ namespace Exceptionless; +/// 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; + /// 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; - ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(min, max); + ArgumentOutOfRangeException.ThrowIfGreaterThan(min, max); return Random.Shared.Next(min, max + 1); } + /// 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"; @@ -48,11 +58,14 @@ public static string GetVersion(string? min, string? max) { return new Version(major, minor, build, revision).ToString(); } + /// 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; - ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(min, max); + ArgumentOutOfRangeException.ThrowIfGreaterThan(min, max); var buf = new byte[8]; Random.Shared.NextBytes(buf); @@ -61,16 +74,24 @@ public static long GetLong(long min, long max) { return (Math.Abs(longRand % (max - min)) + min); } + /// Returns a random across the full range. public static long GetLong() => 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)}"; + /// + /// 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; if (start.HasValue && end.HasValue) - ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(start.Value, end.Value, nameof(start)); + ArgumentOutOfRangeException.ThrowIfGreaterThan(start.Value, end.Value, nameof(start)); start ??= DateTime.MinValue; end ??= DateTime.MaxValue; @@ -81,6 +102,11 @@ public static DateTime GetDateTime(DateTime? start = null, DateTime? end = null) return start.Value + newSpan; } + /// + /// 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)); @@ -94,12 +120,18 @@ public static DateTimeOffset GetDateTimeOffset(DateTimeOffset? start = null, Dat return start.Value + newSpan; } + /// + /// 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) - ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(min.Value, max.Value, nameof(min)); + ArgumentOutOfRangeException.ThrowIfGreaterThan(min.Value, max.Value, nameof(min)); min ??= TimeSpan.Zero; max ??= TimeSpan.MaxValue; @@ -107,18 +139,26 @@ public static TimeSpan GetTimeSpan(TimeSpan? min = null, TimeSpan? max = null) { return min.Value + new TimeSpan((long)(new TimeSpan(max.Value.Ticks - min.Value.Ticks).Ticks * Random.Shared.NextDouble())); } + /// 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; } + /// + /// 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) - ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(min.Value, max.Value, nameof(min)); + ArgumentOutOfRangeException.ThrowIfGreaterThan(min.Value, max.Value, nameof(min)); min ??= Double.MinValue; max ??= Double.MaxValue; @@ -126,18 +166,31 @@ public static double GetDouble(double? min = null, double? max = null) { return Random.Shared.NextDouble() * (max.Value - min.Value) + min.Value; } + /// Returns a random using two random integers as bounds. public static decimal GetDecimal() => GetDecimal(GetInt(), GetInt()); + /// Returns a random in the range [, ]. public static decimal GetDecimal(int min, int max) => (decimal)GetDouble(min, max); + /// 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))!; } + /// 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; @@ -163,19 +216,31 @@ public static string GetString(int minLength = 5, int maxLength = 20, string all // Some characters are left out because they are hard to tell apart. private const string DEFAULT_ALPHA_CHARS = "abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ"; + + /// 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"; + + /// 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); + /// 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); + /// 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; } + /// 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); @@ -188,6 +253,10 @@ public static string GetWords(int minWords = 2, int maxWords = 10, bool titleCas return builder.ToString().Trim(); } + /// 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); @@ -218,6 +287,14 @@ private static string UpperCaseFirstCharacter(string input) { 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); @@ -299,7 +376,11 @@ public static string GetParagraphs(int count = 3, int minSentences = 3, int maxS ]; } +/// 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; From 881351024b2a26aa53b48183bbebc61cdce91241 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Fri, 20 Feb 2026 15:15:06 -0600 Subject: [PATCH 3/3] Simplifies enum test and improves readability. Refactors enum test to use a more descriptive name. Improves code readability by explicitly declaring the type of the result variable in the GetSentencesTest method and marking the _numbers array as readonly. --- test/Exceptionless.RandomData.Tests/RandomDataTests.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/Exceptionless.RandomData.Tests/RandomDataTests.cs b/test/Exceptionless.RandomData.Tests/RandomDataTests.cs index 5a911d3..499fafd 100644 --- a/test/Exceptionless.RandomData.Tests/RandomDataTests.cs +++ b/test/Exceptionless.RandomData.Tests/RandomDataTests.cs @@ -20,21 +20,21 @@ public void RandomDecimal() { [Fact] public void GetEnumWithOneValueTest() { - var result = RandomData.GetEnum<_days>(); + var result = RandomData.GetEnum(); - Assert.Equal<_days>(_days.Monday, result); + Assert.Equal(Days.Monday, result); } [Fact] public void GetSentencesTest() { - var result = RandomData.GetSentence(); + string result = RandomData.GetSentence(); Assert.False(String.IsNullOrEmpty(result)); } - private int[] _numbers = [1, 2, 3]; + private readonly int[] _numbers = [1, 2, 3]; - private enum _days { + private enum Days { Monday } }