diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..8d74243 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "csharpier": { + "version": "1.2.6", + "commands": [ + "csharpier" + ], + "rollForward": false + } + } +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..4b3e93c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,241 @@ +root = true + +############################################## +# All files +############################################## +[*] +indent_style = space +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + +############################################## +# XML project files +############################################## +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj,slnx}] +indent_size = 2 + +############################################## +# Config & data files +############################################## +[*.{json,yml,yaml,toml}] +indent_size = 2 + +[*.xml] +indent_size = 2 + +############################################## +# Markdown +############################################## +[*.md] +trim_trailing_whitespace = false + +############################################## +# C# and VB files +############################################## +[*.{cs,csx,vb,vbx}] +indent_size = 4 +tab_width = 4 + +# ---- .NET code style ---- + +# Prefer language keywords over BCL type names +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion + +# Avoid 'this.' qualification +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_event = false:suggestion + +# Accessibility modifiers required +dotnet_style_require_accessibility_modifiers = always:suggestion + +# Prefer object/collection initializers +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion + +# Prefer collection expressions (C# 12+) +dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion + +# Null checks +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion + +# Auto-properties +dotnet_style_prefer_auto_properties = true:suggestion + +# Compound assignments +dotnet_style_prefer_compound_assignment = true:suggestion + +# Simplified boolean expressions +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion + +# Explicit tuple names +dotnet_style_explicit_tuple_names = true:suggestion + +# Inferred names +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion + +# Parentheses +dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:silent +dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:silent +dotnet_style_parentheses_in_other_binary_operators = never_if_unnecessary:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent + +# ---- C# code style ---- + +# var preferences +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = true:suggestion + +# Expression-bodied members +csharp_style_expression_bodied_methods = when_on_single_line:suggestion +csharp_style_expression_bodied_constructors = false:suggestion +csharp_style_expression_bodied_operators = when_on_single_line:suggestion +csharp_style_expression_bodied_properties = when_on_single_line:suggestion +csharp_style_expression_bodied_indexers = when_on_single_line:suggestion +csharp_style_expression_bodied_accessors = when_on_single_line:suggestion +csharp_style_expression_bodied_lambdas = when_on_single_line:suggestion +csharp_style_expression_bodied_local_functions = when_on_single_line:suggestion + +# Pattern matching +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_prefer_switch_expression = true:suggestion +csharp_style_prefer_pattern_matching = true:suggestion +csharp_style_prefer_not_pattern = true:suggestion +csharp_style_prefer_extended_property_pattern = true:suggestion + +# Null checks +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion + +# Modifier order +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async:suggestion + +# Inlined variable declarations +csharp_style_inlined_variable_declaration = true:suggestion + +# Simplify default +csharp_prefer_simple_default_expression = true:suggestion + +# Index and range operators (C# 8+) +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion + +# Deconstructed variable declarations +csharp_style_deconstructed_variable_declaration = true:suggestion + +# Using directives placement +csharp_using_directive_placement = outside_namespace:suggestion + +# Namespace style (file-scoped, C# 10+) +csharp_style_namespace_declarations = file_scoped:suggestion + +# Primary constructors (C# 12+) +csharp_style_prefer_primary_constructors = true:suggestion + +# Tuple swap (C# 7.1+) +csharp_style_prefer_tuple_swap = true:suggestion + +# Prefer static local functions +csharp_prefer_static_local_function = true:suggestion + +# Local functions over anonymous lambdas +csharp_style_prefer_local_over_anonymous_function = true:suggestion + +# ---- C# formatting ---- + +# New lines +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +# Indentation +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_indent_labels = one_less_than_current + +# Spacing +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_around_binary_operators = before_and_after +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false + +# Wrapping +csharp_preserve_single_line_statements = false +csharp_preserve_single_line_blocks = true + +# ---- Naming conventions ---- + +# Interfaces: IPascalCase +dotnet_naming_rule.interface_naming.severity = suggestion +dotnet_naming_rule.interface_naming.symbols = interfaces +dotnet_naming_rule.interface_naming.style = interface_style + +dotnet_naming_symbols.interfaces.applicable_kinds = interface +dotnet_naming_symbols.interfaces.applicable_accessibilities = * + +dotnet_naming_style.interface_style.required_prefix = I +dotnet_naming_style.interface_style.capitalization = pascal_case + +# Private fields: _camelCase +dotnet_naming_rule.private_field_naming.severity = suggestion +dotnet_naming_rule.private_field_naming.symbols = private_fields +dotnet_naming_rule.private_field_naming.style = private_field_style + +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private, private_protected + +dotnet_naming_style.private_field_style.required_prefix = _ +dotnet_naming_style.private_field_style.capitalization = camel_case + +# Constants: PascalCase +dotnet_naming_rule.constant_naming.severity = suggestion +dotnet_naming_rule.constant_naming.symbols = constants +dotnet_naming_rule.constant_naming.style = pascal_case_style + +dotnet_naming_symbols.constants.applicable_kinds = field +dotnet_naming_symbols.constants.required_modifiers = const + +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +# Public/internal members: PascalCase +dotnet_naming_rule.public_members_naming.severity = suggestion +dotnet_naming_rule.public_members_naming.symbols = public_members +dotnet_naming_rule.public_members_naming.style = pascal_case_style + +dotnet_naming_symbols.public_members.applicable_kinds = class,struct,enum,property,method,event,delegate,namespace +dotnet_naming_symbols.public_members.applicable_accessibilities = public, internal, protected, protected_internal + +# Parameters and locals: camelCase +dotnet_naming_rule.parameter_naming.severity = suggestion +dotnet_naming_rule.parameter_naming.symbols = parameters +dotnet_naming_rule.parameter_naming.style = camel_case_style + +dotnet_naming_symbols.parameters.applicable_kinds = parameter, local + +dotnet_naming_style.camel_case_style.capitalization = camel_case + +# ---- Nullable diagnostics ---- +dotnet_diagnostic.CS8600.severity = warning +dotnet_diagnostic.CS8602.severity = warning +dotnet_diagnostic.CS8603.severity = warning +dotnet_diagnostic.CS8604.severity = warning +dotnet_diagnostic.CS8618.severity = warning diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 2d29079..84dc51c 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -12,11 +12,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup .NET - uses: actions/setup-dotnet@v2 + uses: actions/setup-dotnet@v4 with: - dotnet-version: 6.0.x + dotnet-version: 10.0.x - name: Restore dependencies run: dotnet restore - name: Build diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..7435b19 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,74 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands + +```bash +# Build the entire solution +dotnet build + +# Run all tests +dotnet test + +# Run tests with verbose output +dotnet test --verbosity normal + +# Run a single test by name (partial match works) +dotnet test --filter "FullyQualifiedName~" +# Example: +dotnet test --filter "FullyQualifiedName~Verify_In_Exists" + +# Run all tests in a specific class +dotnet test --filter "ClassName~EnumerableExtensionTest" + +# Build only the library project +dotnet build src/CSharpHelperExtensions/CSharpHelperExtensions.csproj + +# Pack as NuGet package +dotnet pack src/CSharpHelperExtensions/CSharpHelperExtensions.csproj + +# Restore local .NET tools (CSharpier formatter) +dotnet tool restore + +# Build using the solution file explicitly +dotnet build CSharpHelperExtensions.slnx + +# Test using the solution file explicitly +dotnet test CSharpHelperExtensions.slnx +``` + +## Architecture + +This is a two-project solution: + +- **`src/CSharpHelperExtensions/`** — `net10.0` class library. The publishable NuGet package (`CSharpHelperExtensions` v2.0.0). Depends only on `Newtonsoft.Json`. +- **`src/CSharpHelperExtensions.Test/`** — xUnit test project (`net10.0`) using FluentAssertions. + +Root-level config files: `CSharpHelperExtensions.slnx` (solution entry point), `global.json` (pins SDK to 10.0.x), `.editorconfig` (C# code style + formatting rules), `.config/dotnet-tools.json` (CSharpier formatter). + +### Extension method namespaces + +The library splits extensions across three namespaces — callers must import the right one: + +| File | Namespace | Key types | +|------|-----------|-----------| +| `GenericExtensions.cs` | `CSharpHelperExtensions` | `In`, `IsNullOrEmpty` (string), `IsBetween`, `ToJson` | +| `EnumerableExtensions.cs` | `CSharpHelperExtensions.Enumerable` | `IsNullOrEmpty`, `CleanNullOrEmptyItems`, `ContainsOnly`, `AreEqual`, `ForEach`, `Reduce` | +| `StringExtensions.cs` | `CSharpHelperExtensions.Strings` | `ToNullable` | + +`IsNullOrEmpty` exists in **both** `GenericExtensions` (for `string`) and `EnumerableExtensions` (for `IEnumerable`). Be careful about which namespace is imported. + +### `BetweenComparison` enum + +Defined in `GenericExtensions.cs`, controls how `IsBetween` handles bounds: +- `None` (default) — inclusive on both ends +- `ExcludeBoth` — exclusive on both ends +- `ExcludeLower` — excludes lower bound, includes upper +- `ExcludeUpper` — includes lower bound, excludes upper + +### `Compare` enum + +Defined in `EnumerableExtensions.cs`, used by `AreEqual`: +- `NoOrder` (default) — order-insensitive equality +- `InOrder` — positional equality diff --git a/CSharpHelperExtensions.sln b/CSharpHelperExtensions.sln deleted file mode 100644 index 190c028..0000000 --- a/CSharpHelperExtensions.sln +++ /dev/null @@ -1,22 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSharpHelperExtensions", "CSharpHelperExtensions\CSharpHelperExtensions.csproj", "{839755FC-50B8-4252-8895-70CAFC77DD25}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSharpHelperExtensions.Test", "CSharpHelperExtensions.Test\CSharpHelperExtensions.Test.csproj", "{32B44525-DDA0-4420-8700-E24B399CD7FE}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {839755FC-50B8-4252-8895-70CAFC77DD25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {839755FC-50B8-4252-8895-70CAFC77DD25}.Debug|Any CPU.Build.0 = Debug|Any CPU - {839755FC-50B8-4252-8895-70CAFC77DD25}.Release|Any CPU.ActiveCfg = Release|Any CPU - {839755FC-50B8-4252-8895-70CAFC77DD25}.Release|Any CPU.Build.0 = Release|Any CPU - {32B44525-DDA0-4420-8700-E24B399CD7FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {32B44525-DDA0-4420-8700-E24B399CD7FE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {32B44525-DDA0-4420-8700-E24B399CD7FE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {32B44525-DDA0-4420-8700-E24B399CD7FE}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal diff --git a/CSharpHelperExtensions.slnx b/CSharpHelperExtensions.slnx new file mode 100644 index 0000000..8e42b2b --- /dev/null +++ b/CSharpHelperExtensions.slnx @@ -0,0 +1,4 @@ + + + + diff --git a/CSharpHelperExtensions/EnumerableExtensions.cs b/CSharpHelperExtensions/EnumerableExtensions.cs deleted file mode 100644 index f0272c6..0000000 --- a/CSharpHelperExtensions/EnumerableExtensions.cs +++ /dev/null @@ -1,186 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace CSharpHelperExtensions.Enumerable -{ - public enum Compare - { - InOrder, - NoOrder - } - public static class EnumerableExtensions - { - /// - /// Check if the enumerable contains only a particular item - /// - /// - /// - /// - /// - public static bool ContainsOnly(this IEnumerable enumerable,params T[] value) - { - if (value.IsNullOrEmpty() || enumerable.IsNullOrEmpty()) - { - return false; - } - if (enumerable.Count() != value.Count()) - { - return false; - } - return value.All(item => enumerable.Contains(item)); - } - - /// - /// - /// - /// - /// - /// - /// - /// - public static bool AreEqual(this IEnumerable enumerable, IEnumerable values, - Compare comparison = Compare.NoOrder) - { - if (ReferenceEquals(enumerable, null) && ReferenceEquals(values, null)) - { - return true; - } - values ??= new List(); - enumerable ??= new List(); - if (ReferenceEquals(enumerable, values)) - { - return true; - } - if (values.Count() != enumerable.Count()) - { - return false; - } - return comparison switch - { - Compare.InOrder => CompareItemsInOrder(enumerable, values), - Compare.NoOrder => enumerable.All(item => values.Contains(item)), - _ => false - }; - } - - private static bool CompareItemsInOrder(IEnumerable list1, IEnumerable list2) - { - var firstList = list1.ToList(); - var secondList = list2.ToList(); - for(int index =0; index<= firstList.Count - 1; index ++) - { - var areEqual = firstList[index].Equals(secondList[index]); - if (!areEqual) - { - return false; - } - } - return true; - } - - /// - /// Clean nulls and empty items from an IEnumerable, - /// if this is IEnumerable of string this will clean the empty string and whitespace - /// - /// Enumerable to clean - /// type - /// Enumerable cleaned up - public static IEnumerable CleanNullOrEmptyItems(this IEnumerable value) - { - var list = value?.ToList(); - if (list is null || !list.Any()) - { - return null; - } - - return list.Where(item => - { - if (item is string itemStr) - { - return !string.IsNullOrWhiteSpace(itemStr); - } - - return item is not null; - }).ToList(); - } - - /// - /// Check IEnumerable is null, empty or has items that are all null - /// - /// Enumerable to check - /// Type - /// true or false - public static bool IsNullOrEmpty(this IEnumerable values) - { - var enumerable = values?.ToArray(); - return enumerable == null || !enumerable.Any() || enumerable.All(item => item is null); - } - - /// - /// - /// - /// - /// - /// - /// - public static IEnumerable ForEach(this IEnumerable values, Action execute) - { - var collection = values?.ToList() ?? new List(); - foreach (var item in collection) - { - execute(item); - } - - return values; - } - - /// - /// Reduce the result to a single value, runs each items through a reducer function - /// - /// Collection on which to perform reduce - /// Callback function or the reducer function - /// Initial value to start - /// Type of the collection on which the reduce is called - /// Return value type - /// - public static TOut Reduce(this IEnumerable values, Func execute, TOut initialValue = default) - { - var collection = values?.ToList() ?? new List(); - var result = default(TOut); - var temp = initialValue; - foreach (var item in collection) - { - result = execute(item, temp); - temp = result; - } - - return result; - } - - /// - /// - /// - /// - /// - /// - /// - /// - /// - public static TOut Reduce(this IEnumerable values, Func execute, TOut initialValue = default) - { - var collection = values?.ToList() ?? new List(); - var result = default(TOut); - var temp = initialValue; - for (int counter = 0; counter <= collection.Count -1; counter++) - { - var item = collection[counter]; - result = execute(item, temp, counter); - temp = result; - } - - return result; - } - } -} - diff --git a/CSharpHelperExtensions/GenericExtensions.cs b/CSharpHelperExtensions/GenericExtensions.cs deleted file mode 100644 index 961fda1..0000000 --- a/CSharpHelperExtensions/GenericExtensions.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using Newtonsoft.Json; - -namespace CSharpHelperExtensions -{ - public enum BetweenComparison - { - None, - ExcludeBoth, - ExcludeLower, - ExcludeUpper - } - public static class GenericExtensions - { - - /// - /// Check if a value is in between two comparable values - /// default comparison type is None and it will include the lower and upper bounds in the comparison - /// - /// value to compare - /// lower bound value - /// upper bound value - /// - /// type - /// true/ false - public static bool IsBetween(this T value, T lower, T upper, BetweenComparison comparison = BetweenComparison.None) - where T : IComparable - { - return comparison switch - { - BetweenComparison.ExcludeBoth => (value.CompareTo(lower) > 0) && (value.CompareTo(upper) < 0), - BetweenComparison.ExcludeLower => (value.CompareTo(lower) > 0) && (value.CompareTo(upper) <= 0), - BetweenComparison.ExcludeUpper => (value.CompareTo(lower) >= 0) && (value.CompareTo(upper) < 0), - _ => (lower.CompareTo(value) <= 0) && (value.CompareTo(upper) <= 0) - }; - } - - /// - /// Check if the value is part of a list of items - /// - /// Item to check - /// Item to check against - /// Type - /// true or false - public static bool In(this T value, params T[] input) - { - return input is { } && input.Contains(value); - } - - /// - /// Check if a string is null, empty or has whitespace - /// By default the - /// - /// string to check - /// true or false - public static bool IsNullOrEmpty(this string value) - { - return string.IsNullOrWhiteSpace(value); - } - - /// - /// Convert an object to JSON representation - /// - /// object to convert - /// add indentation or not - /// Type - /// JSON String - public static string ToJson(this T value, bool indentation = false) where T : class - { - var formatting = indentation ? Formatting.Indented : Formatting.None; - return value == null ? null : JsonConvert.SerializeObject(value, formatting); - } - } -} \ No newline at end of file diff --git a/CSharpHelperExtensions/StringExtensions.cs b/CSharpHelperExtensions/StringExtensions.cs deleted file mode 100644 index e98e298..0000000 --- a/CSharpHelperExtensions/StringExtensions.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.ComponentModel; - -namespace CSharpHelperExtensions.Strings -{ - public static class StringExtensions - { - /// - /// Convert Nullable type - /// - /// - /// - /// - public static T? ToNullable(this string input) where T : struct - { - try - { - if (input.IsNullOrEmpty()) - { - return null; - } - - var conv = TypeDescriptor.GetConverter(typeof(T)); - return (T?)conv.ConvertFrom(input); - } - catch (Exception e) - { - Console.WriteLine(e); - throw; - } - } - } -} - diff --git a/docs/superpowers/plans/2026-05-26-upgrade-dotnet10-slnx.md b/docs/superpowers/plans/2026-05-26-upgrade-dotnet10-slnx.md new file mode 100644 index 0000000..fc5e992 --- /dev/null +++ b/docs/superpowers/plans/2026-05-26-upgrade-dotnet10-slnx.md @@ -0,0 +1,345 @@ +# Upgrade to .NET 10 + Migrate to .slnx Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Upgrade both projects from `net6.0`/`netstandard2.1` to `net10.0`, update all NuGet packages, migrate the solution file from `.sln` to `.slnx`, and update CI to match. + +**Architecture:** All changes are configuration-only (no source code logic changes). The library project moves from `netstandard2.1` to `net10.0`, the test project from `net6.0` to `net10.0`, NuGet packages are updated to compatible versions, and `dotnet sln migrate` converts the solution format. CI is updated last. + +**Tech Stack:** .NET 10 SDK (10.0.201), xUnit 2.x, FluentAssertions 7.x, GitHub Actions + +--- + +## Files Modified + +| File | Change | +|------|--------| +| `CSharpHelperExtensions/CSharpHelperExtensions.csproj` | `netstandard2.1` → `net10.0`, `LangVersion` → `latest`, Newtonsoft.Json update | +| `CSharpHelperExtensions.Test/CSharpHelperExtensions.Test.csproj` | `net6.0` → `net10.0`, all package updates | +| `CSharpHelperExtensions.sln` | Deleted after migration | +| `CSharpHelperExtensions.slnx` | Created by `dotnet sln migrate` | +| `.github/workflows/dotnet.yml` | `dotnet-version: 6.0.x` → `10.0.x`, action versions bumped | +| `global.json` | New file — pins SDK to 10.0.x | +| `CLAUDE.md` | Remove the runtime mismatch warning | + +--- + +## Task 1: Upgrade the library project to .NET 10 + +**Files:** +- Modify: `CSharpHelperExtensions/CSharpHelperExtensions.csproj` + +- [ ] **Step 1: Replace the csproj content** + +Open `CSharpHelperExtensions/CSharpHelperExtensions.csproj` and replace it with: + +```xml + + + + CSharpHelperExtensions + 1.0.0 + Bipin Radhakrishnan + net10.0 + 2.0.0 + A set of helper extension methods that are used very often when coding + https://github.com/rbipin/dry-extensions-csharp + git + + CSharpHelperExtensions + CSharpHelperExtensions + latest + + + + + + + +``` + +- [ ] **Step 2: Build the library to confirm it compiles** + +```bash +dotnet build CSharpHelperExtensions/CSharpHelperExtensions.csproj +``` + +Expected: `Build succeeded.` + +- [ ] **Step 3: Commit** + +```bash +git add CSharpHelperExtensions/CSharpHelperExtensions.csproj +git commit -m "chore: upgrade library to net10.0, LangVersion latest, Newtonsoft.Json 13.0.3" +``` + +--- + +## Task 2: Upgrade the test project to .NET 10 + +**Files:** +- Modify: `CSharpHelperExtensions.Test/CSharpHelperExtensions.Test.csproj` + +- [ ] **Step 1: Replace the test csproj content** + +Open `CSharpHelperExtensions.Test/CSharpHelperExtensions.Test.csproj` and replace it with: + +```xml + + + + net10.0 + false + ReusableExtensions.Unittest + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + +``` + +> **Note on FluentAssertions 7.x:** FA 7 introduces `using FluentAssertions;` namespace change in some edge cases, but `Should().BeTrue()`, `Should().BeFalse()`, `Should().Be()`, `Should().Equal()`, `Should().BeNull()`, `Should().BeOfType()` — all used in this project — are unchanged. + +- [ ] **Step 2: Restore packages** + +```bash +dotnet restore CSharpHelperExtensions.Test/CSharpHelperExtensions.Test.csproj +``` + +Expected: `Restore succeeded.` + +- [ ] **Step 3: Run all tests to verify everything passes** + +```bash +dotnet test --verbosity normal +``` + +Expected output (all 16 tests pass): +``` +Passed! - Failed: 0, Passed: 16, Skipped: 0, Total: 16 +``` + +If any test fails due to a FluentAssertions API change, check the FA 7.x migration guide at https://fluentassertions.com/upgradingtov7 + +- [ ] **Step 4: Commit** + +```bash +git add CSharpHelperExtensions.Test/CSharpHelperExtensions.Test.csproj +git commit -m "chore: upgrade test project to net10.0, update all test packages" +``` + +--- + +## Task 3: Migrate solution file from .sln to .slnx + +**Files:** +- Delete: `CSharpHelperExtensions.sln` +- Create: `CSharpHelperExtensions.slnx` (auto-generated) + +- [ ] **Step 1: Run the migration command** + +```bash +cd /Users/bipin/repo/CSharpHelperExtensions +dotnet sln migrate CSharpHelperExtensions.sln +``` + +Expected: A new file `CSharpHelperExtensions.slnx` is created in the same directory. + +> The `.slnx` format is XML-based and human-readable. It was introduced in .NET 9 SDK and is supported by Visual Studio 2022 17.x+ and VS Code with the C# Dev Kit extension. + +- [ ] **Step 2: Verify the .slnx is valid by building through it** + +```bash +dotnet build CSharpHelperExtensions.slnx +``` + +Expected: `Build succeeded.` + +- [ ] **Step 3: Run tests through the new solution file** + +```bash +dotnet test CSharpHelperExtensions.slnx --verbosity normal +``` + +Expected: `Passed! - Failed: 0, Passed: 16, Skipped: 0, Total: 16` + +- [ ] **Step 4: Delete the old .sln file** + +```bash +rm CSharpHelperExtensions.sln +``` + +- [ ] **Step 5: Commit** + +```bash +git add CSharpHelperExtensions.slnx +git rm CSharpHelperExtensions.sln +git commit -m "chore: migrate solution from .sln to .slnx format" +``` + +--- + +## Task 4: Add global.json to pin the SDK version + +**Files:** +- Create: `global.json` + +- [ ] **Step 1: Create global.json** + +Create `/Users/bipin/repo/CSharpHelperExtensions/global.json`: + +```json +{ + "sdk": { + "version": "10.0.201", + "rollForward": "latestMinor" + } +} +``` + +> `rollForward: latestMinor` means it will use any newer patch/minor of the 10.x SDK if available, but won't silently jump to .NET 11. + +- [ ] **Step 2: Verify build still works** + +```bash +dotnet build +``` + +Expected: `Build succeeded.` + +- [ ] **Step 3: Commit** + +```bash +git add global.json +git commit -m "chore: add global.json to pin SDK to 10.0.x" +``` + +--- + +## Task 5: Update CI/CD pipeline + +**Files:** +- Modify: `.github/workflows/dotnet.yml` + +- [ ] **Step 1: Replace the workflow content** + +Open `.github/workflows/dotnet.yml` and replace it with: + +```yaml +name: .NET + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + - name: Restore dependencies + run: dotnet restore + - name: Build + run: dotnet build --no-restore + - name: Test + run: dotnet test --no-build --verbosity normal +``` + +Changes from original: +- `actions/checkout@v3` → `@v4` +- `actions/setup-dotnet@v2` → `@v4` +- `dotnet-version: 6.0.x` → `10.0.x` + +- [ ] **Step 2: Commit** + +```bash +git add .github/workflows/dotnet.yml +git commit -m "ci: upgrade GitHub Actions to .NET 10 and bump action versions" +``` + +--- + +## Task 6: Update CLAUDE.md + +**Files:** +- Modify: `CLAUDE.md` + +- [ ] **Step 1: Remove the runtime mismatch warning** + +Find and remove this block from `CLAUDE.md`: + +``` +> ⚠️ **Runtime mismatch:** The test project targets `net6.0`, but the machine has .NET 8 and .NET 10 installed. Tests will fail to run until the test project's `TargetFramework` in `CSharpHelperExtensions.Test/CSharpHelperExtensions.Test.csproj` is updated to `net8.0` or `net10.0`. +``` + +- [ ] **Step 2: Update solution file reference** + +In the Commands section of `CLAUDE.md`, add a note that the solution file is now `.slnx`: + +```bash +# Build using the solution file explicitly +dotnet build CSharpHelperExtensions.slnx + +# Test using the solution file explicitly +dotnet test CSharpHelperExtensions.slnx +``` + +- [ ] **Step 3: Commit** + +```bash +git add CLAUDE.md +git commit -m "docs: update CLAUDE.md for .NET 10 and .slnx solution" +``` + +--- + +## Verification + +Full end-to-end check after all tasks: + +```bash +# Full clean build +dotnet build + +# All tests pass +dotnet test --verbosity normal + +# Confirm .sln is gone and .slnx is present +ls *.sln* *.slnx + +# Confirm SDK version in use +dotnet --version +``` + +Expected final state: +- `dotnet --version` → `10.0.201` (or later 10.x) +- `dotnet test` → `Passed! - Failed: 0, Passed: 16, Skipped: 0, Total: 16` +- Only `CSharpHelperExtensions.slnx` exists (no `.sln`) diff --git a/docs/superpowers/plans/2026-05-26-xml-doc-audit.md b/docs/superpowers/plans/2026-05-26-xml-doc-audit.md new file mode 100644 index 0000000..7233763 --- /dev/null +++ b/docs/superpowers/plans/2026-05-26-xml-doc-audit.md @@ -0,0 +1,513 @@ +# XML Documentation Audit & Update Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Audit and rewrite all XML documentation comments on public members in the three extension-method source files so that library consumers get accurate, complete, useful IntelliSense and generated docs. + +**Architecture:** Pure documentation pass — no logic changes. Each source file is updated in one commit. Build verification confirms no XML-doc compiler errors. + +**Tech Stack:** C# 13 / .NET 10, xUnit, `dotnet build` + +--- + +## Issues Found + +| Location | Problem | +|---|---| +| `BetweenComparison` enum | No XML docs on type or any value | +| `IsBetween` | `` is blank; `` is "true/ false" | +| `In` | `input` param says "Item to check against" — it's a `params` array | +| `IsNullOrEmpty` (string) | Summary ends mid-sentence: "By default the " | +| `ToJson` | `indentation` param has no description; null-return case undocumented | +| `Compare` enum | No XML docs on type or any value | +| `ContainsOnly` | All params and `` empty | +| `AreEqual` | Entire `` is blank; all params and `` empty | +| `CleanNullOrEmptyItems` | Null-input return behavior not documented | +| `IsNullOrEmpty` | Param tag says `value` but parameter is `values` | +| `ForEach` | All params and `` empty | +| `Reduce` (no index) | `initialValue` description truncated; `` empty | +| `Reduce` (with index) | Entire `` blank; all params and `` empty | +| `ToNullable` | Summary too brief; all params and `` empty; no `` tag | + +--- + +## Files Modified + +- `src/CSharpHelperExtensions/GenericExtensions.cs` +- `src/CSharpHelperExtensions/EnumerableExtensions.cs` +- `src/CSharpHelperExtensions/StringExtensions.cs` + +--- + +## Task 1: Update GenericExtensions.cs + +**Files:** +- Modify: `src/CSharpHelperExtensions/GenericExtensions.cs` + +- [ ] **Step 1: Apply updated XML docs** + +Replace the entire XML-doc block for `BetweenComparison`, `IsBetween`, `In`, `IsNullOrEmpty`, and `ToJson` with the following. Leave all method bodies untouched. + +**`BetweenComparison` enum** — add summary above the enum declaration and a doc comment on each value: + +```csharp +/// +/// Controls which bounds are included when using . +/// +public enum BetweenComparison +{ + /// Inclusive on both ends: lower ≤ value ≤ upper. This is the default. + None, + /// Exclusive on both ends: lower < value < upper. + ExcludeBoth, + /// Exclusive lower bound, inclusive upper: lower < value ≤ upper. + ExcludeLower, + /// Inclusive lower bound, exclusive upper: lower ≤ value < upper. + ExcludeUpper +} +``` + +**`IsBetween`:** + +```csharp +/// +/// Determines whether a value falls within the range defined by and . +/// +/// The value to test. +/// The lower bound of the range. +/// The upper bound of the range. +/// +/// Controls which bounds are included in the comparison. +/// Defaults to , which is inclusive on both ends (lower ≤ value ≤ upper). +/// +/// Any type that implements . +/// +/// if satisfies the range check; +/// otherwise . +/// +/// +/// +/// 5.IsBetween(1, 10) // true (inclusive both ends) +/// 1.IsBetween(1, 10) // true (lower bound included) +/// 1.IsBetween(1, 10, BetweenComparison.ExcludeLower) // false (lower bound excluded) +/// 10.IsBetween(1, 10, BetweenComparison.ExcludeUpper) // false (upper bound excluded) +/// 10.IsBetween(1, 10, BetweenComparison.ExcludeBoth) // false (both bounds excluded) +/// +/// +``` + +**`In`:** + +```csharp +/// +/// Determines whether a value equals any item in a given set. +/// Equivalent to SQL's IN operator. +/// +/// The value to look for. +/// One or more candidate values to match against. +/// The type of the value and candidates. +/// +/// if matches any item in ; +/// otherwise . +/// +/// +/// +/// "admin".In("admin", "superadmin") // true +/// "guest".In("admin", "superadmin") // false +/// 3.In(1, 2, 3, 4) // true +/// +/// +``` + +**`IsNullOrEmpty(string)`:** + +```csharp +/// +/// Returns if the string is , empty, +/// or consists only of whitespace characters. +/// Delegates to . +/// +/// +/// This overload operates on . +/// For collections, use +/// +/// (requires the CSharpHelperExtensions.Enumerable namespace). +/// +/// The string to check. +/// +/// if is , empty, or whitespace-only; +/// otherwise . +/// +/// +/// +/// ((string)null).IsNullOrEmpty() // true +/// "".IsNullOrEmpty() // true +/// " ".IsNullOrEmpty() // true (whitespace only) +/// "hello".IsNullOrEmpty() // false +/// +/// +``` + +**`ToJson`:** + +```csharp +/// +/// Serializes an object to its JSON string representation using Newtonsoft.Json. +/// +/// +/// The object to serialize. Returns when this argument is . +/// +/// +/// When , the JSON output is pretty-printed with indentation. +/// Defaults to (compact, single-line output). +/// +/// The type of the object to serialize. Must be a reference type. +/// +/// A JSON string representing , +/// or if is . +/// +/// +/// +/// var obj = new { Name = "Alice", Age = 30 }; +/// obj.ToJson() // {"Name":"Alice","Age":30} +/// obj.ToJson(indentation: true) +/// // { +/// // "Name": "Alice", +/// // "Age": 30 +/// // } +/// ((object)null).ToJson() // null +/// +/// +``` + +- [ ] **Step 2: Build and verify** + +```bash +cd /Users/bipin/repo/CSharpHelperExtensions && dotnet build src/CSharpHelperExtensions/CSharpHelperExtensions.csproj +``` + +Expected: `Build succeeded. 0 Error(s)` + +- [ ] **Step 3: Commit** + +```bash +git add src/CSharpHelperExtensions/GenericExtensions.cs +git commit -m "docs: improve XML documentation on GenericExtensions public members" +``` + +--- + +## Task 2: Update EnumerableExtensions.cs + +**Files:** +- Modify: `src/CSharpHelperExtensions/EnumerableExtensions.cs` + +- [ ] **Step 1: Apply updated XML docs** + +**`Compare` enum** — add summary above the enum declaration and a doc comment on each value: + +```csharp +/// +/// Controls how two sequences are compared by . +/// +public enum Compare +{ + /// Elements must appear in the same positional order in both sequences. + InOrder, + /// + /// Sequences are equal if they contain the same elements regardless of order. + /// This is the default. + /// + NoOrder +} +``` + +**`ContainsOnly`:** + +```csharp +/// +/// Returns if the sequence contains exactly the specified items — +/// no more, no fewer — regardless of order. +/// +/// The element type. +/// The sequence to inspect. +/// The exact set of expected items. +/// +/// if has the same count as +/// and every item in appears in . +/// Returns if either argument is or empty, +/// or if the element sets differ. +/// +/// +/// +/// new[] { 1, 2, 3 }.ContainsOnly(3, 1, 2) // true (order doesn't matter) +/// new[] { 1, 2, 3 }.ContainsOnly(1, 2) // false (extra element in source) +/// new[] { 1, 2 }.ContainsOnly(1, 2, 3) // false (missing element in source) +/// +/// +``` + +**`AreEqual`:** + +```csharp +/// +/// Determines whether two sequences contain the same elements. +/// Use to choose between order-sensitive and order-insensitive equality. +/// Both sequences being is treated as equal. +/// +/// The element type. +/// The first sequence. +/// The second sequence to compare against. +/// +/// Controls whether element order matters. +/// Defaults to (order-insensitive). +/// +/// +/// if both sequences are equal under the chosen mode; +/// if their counts differ or any element does not match. +/// +/// +/// +/// new[] { 1, 2, 3 }.AreEqual(new[] { 3, 1, 2 }) // true (NoOrder) +/// new[] { 1, 2, 3 }.AreEqual(new[] { 3, 1, 2 }, Compare.InOrder) // false (order differs) +/// new[] { 1, 2, 3 }.AreEqual(new[] { 1, 2, 3 }, Compare.InOrder) // true +/// ((IEnumerable<int>)null).AreEqual(null) // true (both null) +/// +/// +``` + +**`CleanNullOrEmptyItems`:** + +```csharp +/// +/// Returns a new sequence with all elements removed. +/// When is , empty strings and whitespace-only strings +/// are also removed. +/// +/// +/// The sequence to clean. +/// Returns if the input is or empty. +/// +/// +/// The element type. String sequences get additional empty/whitespace filtering. +/// +/// +/// A cleaned with invalid elements removed, +/// or if the input is or contains no items. +/// +/// +/// +/// new[] { "hello", null, "", " ", "world" }.CleanNullOrEmptyItems() +/// // ["hello", "world"] +/// +/// new int?[] { 1, null, 2, null, 3 }.CleanNullOrEmptyItems() +/// // [1, 2, 3] +/// +/// +``` + +**`IsNullOrEmpty`:** + +```csharp +/// +/// Returns if the sequence is , contains no elements, +/// or contains only items. +/// +/// The sequence to check. +/// The element type. +/// +/// if is , empty, +/// or every element is ; otherwise . +/// +/// +/// +/// ((IEnumerable<int>)null).IsNullOrEmpty() // true +/// new List<string>().IsNullOrEmpty() // true +/// new[] { (string)null, null }.IsNullOrEmpty() // true (all-null items) +/// new[] { 1, 2, 3 }.IsNullOrEmpty() // false +/// +/// +``` + +**`ForEach`:** + +```csharp +/// +/// Executes an action on each element of the sequence and returns the original sequence unchanged. +/// Useful for chaining side-effectful operations in a fluent pipeline. +/// +/// +/// The sequence to iterate. +/// If , the action is not invoked and is returned. +/// +/// The action to run for each element. +/// The element type. +/// The original reference (not a copy). +/// +/// +/// var log = new List<string>(); +/// new[] { "a", "b", "c" } +/// .ForEach(item => log.Add(item.ToUpper())) +/// .ForEach(item => Console.WriteLine(item)); +/// // log == ["A", "B", "C"] +/// // original sequence ["a", "b", "c"] is printed to console +/// +/// +``` + +**`Reduce` (without index):** + +```csharp +/// +/// Reduces a sequence to a single accumulated value by repeatedly applying a reducer function. +/// Equivalent to JavaScript's Array.prototype.reduce(). +/// +/// +/// The sequence to reduce. +/// If or empty, returns the default value of . +/// +/// +/// The reducer function. Receives the current element and the current accumulated value, +/// and returns the new accumulated value. +/// +/// +/// The starting value for the accumulator before the first element is processed. +/// Defaults to (). +/// +/// The element type of the input sequence. +/// The type of the accumulated result. +/// The final accumulated value after all elements have been processed. +/// +/// +/// // Sum integers +/// new[] { 1, 2, 3, 4 }.Reduce((item, acc) => acc + item, initialValue: 0) // 10 +/// +/// // Build a comma-separated string +/// new[] { "a", "b", "c" } +/// .Reduce((item, acc) => acc == "" ? item : acc + ", " + item, "") // "a, b, c" +/// +/// +``` + +**`Reduce` (with index):** + +```csharp +/// +/// Reduces a sequence to a single accumulated value by repeatedly applying a reducer function +/// that also receives the current element's zero-based index. +/// +/// +/// The sequence to reduce. +/// If or empty, returns the default value of . +/// +/// +/// The reducer function. Receives the current element, the current accumulated value, +/// and the zero-based index of the current element; returns the new accumulated value. +/// +/// +/// The starting value for the accumulator before the first element is processed. +/// Defaults to (). +/// +/// The element type of the input sequence. +/// The type of the accumulated result. +/// The final accumulated value after all elements have been processed. +/// +/// +/// // Build indexed labels +/// new[] { "apple", "banana", "cherry" } +/// .Reduce((item, acc, index) => acc + $"{index}: {item}\n", "") +/// // "0: apple\n1: banana\n2: cherry\n" +/// +/// +``` + +- [ ] **Step 2: Build and verify** + +```bash +cd /Users/bipin/repo/CSharpHelperExtensions && dotnet build src/CSharpHelperExtensions/CSharpHelperExtensions.csproj +``` + +Expected: `Build succeeded. 0 Error(s)` + +- [ ] **Step 3: Commit** + +```bash +git add src/CSharpHelperExtensions/EnumerableExtensions.cs +git commit -m "docs: improve XML documentation on EnumerableExtensions public members" +``` + +--- + +## Task 3: Update StringExtensions.cs + +**Files:** +- Modify: `src/CSharpHelperExtensions/StringExtensions.cs` + +- [ ] **Step 1: Apply updated XML docs** + +**`ToNullable`:** + +```csharp +/// +/// Converts a string to the specified nullable value type using its registered . +/// Returns when the input is , empty, or whitespace. +/// +/// The string to convert. +/// +/// The target value type (e.g. , , ). +/// Must be a struct; the return type is ?. +/// +/// +/// The converted value wrapped in a nullable, +/// or if is , empty, or whitespace. +/// +/// +/// Rethrows any exception thrown by the underlying when the string +/// cannot be parsed as (e.g. +/// or ). +/// +/// +/// +/// "42".ToNullable<int>() // (int?)42 +/// "".ToNullable<int>() // null +/// " ".ToNullable<int>() // null (whitespace) +/// "2024-01-15".ToNullable<DateTime>() // (DateTime?)new DateTime(2024, 1, 15) +/// "not-a-number".ToNullable<int>() // throws FormatException +/// +/// +``` + +- [ ] **Step 2: Build and verify** + +```bash +cd /Users/bipin/repo/CSharpHelperExtensions && dotnet build src/CSharpHelperExtensions/CSharpHelperExtensions.csproj +``` + +Expected: `Build succeeded. 0 Error(s)` + +- [ ] **Step 3: Commit** + +```bash +git add src/CSharpHelperExtensions/StringExtensions.cs +git commit -m "docs: improve XML documentation on StringExtensions.ToNullable" +``` + +--- + +## Task 4: Full solution build and test + +- [ ] **Step 1: Build full solution** + +```bash +cd /Users/bipin/repo/CSharpHelperExtensions && dotnet build +``` + +Expected: `Build succeeded. 0 Error(s) 0 Warning(s)` + +- [ ] **Step 2: Run all tests** + +```bash +cd /Users/bipin/repo/CSharpHelperExtensions && dotnet test --verbosity normal +``` + +Expected: All tests pass. Test output should show no failures. diff --git a/global.json b/global.json new file mode 100644 index 0000000..0631631 --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "10.0.201", + "rollForward": "latestMinor" + } +} diff --git a/sample.ipynb b/sample.ipynb index dc37d65..b35d6dc 100644 --- a/sample.ipynb +++ b/sample.ipynb @@ -2,32 +2,44 @@ "cells": [ { "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "ename": "AttributeError", - "evalue": "'str' object has no attribute 'In'", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)", - "\u001b[1;32m/Users/BRadhakrishnan/Documents/repo/personal/dry-extensions-csharp/sample.ipynb Cell 1\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[39m#r \"./DryExtensions/bin/Debug/netstandard2.1/DryExtensions.dll\"\u001b[39;00m\n\u001b[0;32m----> 2\u001b[0m \u001b[39m\"\u001b[39;49m\u001b[39mMagic\u001b[39;49m\u001b[39m\"\u001b[39;49m\u001b[39m.\u001b[39;49mIn(\u001b[39m\"\u001b[39m\u001b[39mMagic\u001b[39m\u001b[39m\"\u001b[39m, \u001b[39m\"\u001b[39m\u001b[39mBean\u001b[39m\u001b[39m\"\u001b[39m, \u001b[39m\"\u001b[39m\u001b[39mStalk\u001b[39m\u001b[39m\"\u001b[39m)\n", - "\u001b[0;31mAttributeError\u001b[0m: 'str' object has no attribute 'In'" - ] + "execution_count": null, + "metadata": { + "language_info": { + "name": "polyglot-notebook" + }, + "polyglot_notebook": { + "kernelName": "csharp" + }, + "vscode": { + "languageId": "python" } - ], + }, + "outputs": [], "source": [ - "#r \"./DryExtensions/bin/Debug/netstandard2.1/DryExtensions.dll\"\n", + "#r \"./src/CSharpHelperExtensions/bin/Debug/net10.0/CSharpHelperExtensions.dll\"\n", "\"Magic\".In(\"Magic\", \"Bean\", \"Stalk\")\n" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "language_info": { + "name": "polyglot-notebook" + }, + "polyglot_notebook": { + "kernelName": "csharp" + } + }, + "outputs": [], + "source": [] } ], "metadata": { "kernelspec": { - "display_name": "Python 3.10.4 64-bit", - "language": "python", - "name": "python3" + "display_name": ".NET (C#)", + "language": "C#", + "name": ".net-csharp" }, "language_info": { "codemirror_mode": { @@ -36,12 +48,23 @@ }, "file_extension": ".py", "mimetype": "text/x-python", - "name": "python", + "name": "polyglot-notebook", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.4" }, "orig_nbformat": 4, + "polyglot_notebook": { + "kernelInfo": { + "defaultKernelName": "csharp", + "items": [ + { + "aliases": [], + "name": "csharp" + } + ] + } + }, "vscode": { "interpreter": { "hash": "aee8b7b246df8f9039afb4144a1f6fd8d2ca17a180786b69acc140d282b71a49" diff --git a/CSharpHelperExtensions.Test/CSharpHelperExtensions.Test.csproj b/src/CSharpHelperExtensions.Test/CSharpHelperExtensions.Test.csproj similarity index 81% rename from CSharpHelperExtensions.Test/CSharpHelperExtensions.Test.csproj rename to src/CSharpHelperExtensions.Test/CSharpHelperExtensions.Test.csproj index e367717..437926f 100644 --- a/CSharpHelperExtensions.Test/CSharpHelperExtensions.Test.csproj +++ b/src/CSharpHelperExtensions.Test/CSharpHelperExtensions.Test.csproj @@ -1,22 +1,20 @@ - net6.0 - + net10.0 false - ReusableExtensions.Unittest - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -29,4 +27,5 @@ + diff --git a/CSharpHelperExtensions.Test/EnumerableExtensionTest.cs b/src/CSharpHelperExtensions.Test/EnumerableExtensionTest.cs similarity index 100% rename from CSharpHelperExtensions.Test/EnumerableExtensionTest.cs rename to src/CSharpHelperExtensions.Test/EnumerableExtensionTest.cs diff --git a/CSharpHelperExtensions.Test/GenericExtensionTest.cs b/src/CSharpHelperExtensions.Test/GenericExtensionTest.cs similarity index 100% rename from CSharpHelperExtensions.Test/GenericExtensionTest.cs rename to src/CSharpHelperExtensions.Test/GenericExtensionTest.cs diff --git a/CSharpHelperExtensions.Test/StringExtensionTest.cs b/src/CSharpHelperExtensions.Test/StringExtensionTest.cs similarity index 100% rename from CSharpHelperExtensions.Test/StringExtensionTest.cs rename to src/CSharpHelperExtensions.Test/StringExtensionTest.cs diff --git a/CSharpHelperExtensions/CSharpHelperExtensions.csproj b/src/CSharpHelperExtensions/CSharpHelperExtensions.csproj similarity index 79% rename from CSharpHelperExtensions/CSharpHelperExtensions.csproj rename to src/CSharpHelperExtensions/CSharpHelperExtensions.csproj index c922d6a..dbaa6ff 100644 --- a/CSharpHelperExtensions/CSharpHelperExtensions.csproj +++ b/src/CSharpHelperExtensions/CSharpHelperExtensions.csproj @@ -2,23 +2,21 @@ CSharpHelperExtensions - 1.0.0 + 2.0.0 Bipin Radhakrishnan - netstandard2.1 - 1.0.1 + net10.0 + 2.0.0 A set of helper extension methods that are used very often when coding https://github.com/rbipin/dry-extensions-csharp git CSharpHelperExtensions CSharpHelperExtensions - 9 + latest - + - - diff --git a/src/CSharpHelperExtensions/EnumerableExtensions.cs b/src/CSharpHelperExtensions/EnumerableExtensions.cs new file mode 100644 index 0000000..ed67888 --- /dev/null +++ b/src/CSharpHelperExtensions/EnumerableExtensions.cs @@ -0,0 +1,321 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace CSharpHelperExtensions.Enumerable; + +/// +/// Controls how two sequences are compared by . +/// +public enum Compare +{ + /// Elements must appear in the same positional order in both sequences. + InOrder, + /// + /// Sequences are equal if they contain the same elements regardless of order. + /// This is the default. + /// + NoOrder +} +public static class EnumerableExtensions +{ + /// + /// Returns if the sequence contains exactly the specified items — + /// no more, no fewer — regardless of order. + /// + /// The element type. + /// The sequence to inspect. + /// The exact set of expected items. + /// + /// if has the same count as + /// and every item in appears in . + /// Returns if either argument is or empty, + /// or if the element sets differ. + /// + /// + /// Duplicate handling: the check uses internally, + /// so sequences with repeated elements may produce unexpected results. + /// For example, new[] { 1, 2, 2 }.ContainsOnly(1, 1, 2) returns + /// because counts match and every item in the expected set appears somewhere in the source. + /// + /// + /// + /// new[] { 1, 2, 3 }.ContainsOnly(3, 1, 2) // true (order doesn't matter) + /// new[] { 1, 2, 3 }.ContainsOnly(1, 2) // false (extra element in source) + /// new[] { 1, 2 }.ContainsOnly(1, 2, 3) // false (missing element in source) + /// + /// + public static bool ContainsOnly(this IEnumerable enumerable, params T[] value) + { + if (value.IsNullOrEmpty() || enumerable.IsNullOrEmpty()) + { + return false; + } + if (enumerable.Count() != value.Count()) + { + return false; + } + return value.All(item => enumerable.Contains(item)); + } + + /// + /// Determines whether two sequences contain the same elements. + /// Use to choose between order-sensitive and order-insensitive equality. + /// Both sequences being is treated as equal, + /// and a sequence is considered equal to an empty sequence. + /// + /// The element type. + /// The first sequence. + /// The second sequence to compare against. + /// + /// Controls whether element order matters. + /// Defaults to (order-insensitive). + /// + /// + /// if both sequences are equal under the chosen mode; + /// if their counts differ or any element does not match. + /// + /// + /// When using , equality is checked via Contains on each element, + /// so sequences with duplicate elements may compare as equal even if multiplicities differ. + /// For example, new[] { 1, 1, 2 }.AreEqual(new[] { 1, 2, 2 }) returns + /// because both have count 3 and every element of the first appears in the second. + /// Use for strict positional equality. + /// + /// + /// + /// new[] { 1, 2, 3 }.AreEqual(new[] { 3, 1, 2 }) // true (NoOrder) + /// new[] { 1, 2, 3 }.AreEqual(new[] { 3, 1, 2 }, Compare.InOrder) // false (order differs) + /// new[] { 1, 2, 3 }.AreEqual(new[] { 1, 2, 3 }, Compare.InOrder) // true + /// ((IEnumerable<int>)null).AreEqual(null) // true (both null) + /// ((IEnumerable<int>)null).AreEqual(new List<int>()) // true (null == empty) + /// + /// + public static bool AreEqual(this IEnumerable enumerable, IEnumerable values, + Compare comparison = Compare.NoOrder) + { + if (ReferenceEquals(enumerable, null) && ReferenceEquals(values, null)) + { + return true; + } + values ??= new List(); + enumerable ??= new List(); + if (ReferenceEquals(enumerable, values)) + { + return true; + } + if (values.Count() != enumerable.Count()) + { + return false; + } + return comparison switch + { + Compare.InOrder => CompareItemsInOrder(enumerable, values), + Compare.NoOrder => enumerable.All(item => values.Contains(item)), + _ => false + }; + } + + private static bool CompareItemsInOrder(IEnumerable list1, IEnumerable list2) + { + var firstList = list1.ToList(); + var secondList = list2.ToList(); + for (int index = 0; index <= firstList.Count - 1; index++) + { + var areEqual = firstList[index].Equals(secondList[index]); + if (!areEqual) + { + return false; + } + } + return true; + } + + /// + /// Returns a new sequence with all elements removed. + /// When is , empty strings and whitespace-only strings + /// are also removed. + /// + /// + /// The sequence to clean. + /// Returns if the input is or empty. + /// + /// + /// The element type. String sequences get additional empty/whitespace filtering. + /// + /// + /// A cleaned with invalid elements removed, + /// or if the input is or contains no items. + /// + /// + /// + /// new[] { "hello", null, "", " ", "world" }.CleanNullOrEmptyItems() + /// // ["hello", "world"] + /// + /// new int?[] { 1, null, 2, null, 3 }.CleanNullOrEmptyItems() + /// // [1, 2, 3] + /// + /// + public static IEnumerable CleanNullOrEmptyItems(this IEnumerable value) + { + var list = value?.ToList(); + if (list is null || !list.Any()) + { + return null; + } + + return list.Where(item => + { + if (item is string itemStr) + { + return !string.IsNullOrWhiteSpace(itemStr); + } + + return item is not null; + }).ToList(); + } + + /// + /// Returns if the sequence is , contains no elements, + /// or contains only items. + /// + /// The sequence to check. + /// The element type. + /// + /// if is , empty, + /// or every element is ; otherwise . + /// + /// + /// + /// ((IEnumerable<int>)null).IsNullOrEmpty() // true + /// new List<string>().IsNullOrEmpty() // true + /// new[] { (string)null, null }.IsNullOrEmpty() // true (all-null items) + /// new[] { 1, 2, 3 }.IsNullOrEmpty() // false + /// + /// + public static bool IsNullOrEmpty(this IEnumerable values) + { + var enumerable = values?.ToArray(); + return enumerable == null || !enumerable.Any() || enumerable.All(item => item is null); + } + + /// + /// Executes an action on each element of the sequence and returns the original sequence unchanged. + /// Useful for chaining side-effectful operations in a fluent pipeline. + /// + /// + /// The sequence to iterate. + /// If , the action is not invoked and is returned. + /// + /// The action to run for each element. + /// The element type. + /// The original reference (not a copy). + /// + /// + /// var log = new List<string>(); + /// new[] { "a", "b", "c" } + /// .ForEach(item => log.Add(item.ToUpper())) + /// .ForEach(item => Console.WriteLine(item)); + /// // log == ["A", "B", "C"] + /// // original sequence ["a", "b", "c"] is printed to console + /// + /// + public static IEnumerable ForEach(this IEnumerable values, Action execute) + { + var collection = values?.ToList() ?? new List(); + foreach (var item in collection) + { + execute(item); + } + + return values; + } + + /// + /// Reduces a sequence to a single accumulated value by repeatedly applying a reducer function. + /// Equivalent to JavaScript's Array.prototype.reduce(). + /// + /// + /// The sequence to reduce. + /// If or empty, is ignored and + /// () is returned. + /// + /// + /// The reducer function. Receives the current element and the current accumulated value, + /// and returns the new accumulated value. + /// + /// + /// The starting value for the accumulator before the first element is processed. + /// Defaults to (). + /// + /// The element type of the input sequence. + /// The type of the accumulated result. + /// The final accumulated value after all elements have been processed. + /// + /// + /// // Sum integers + /// new[] { 1, 2, 3, 4 }.Reduce((item, acc) => acc + item, initialValue: 0) // 10 + /// + /// // Build a comma-separated string + /// new[] { "a", "b", "c" } + /// .Reduce((item, acc) => acc == "" ? item : acc + ", " + item, "") // "a, b, c" + /// + /// + public static TOut Reduce(this IEnumerable values, Func execute, TOut initialValue = default) + { + var collection = values?.ToList() ?? new List(); + var result = default(TOut); + var temp = initialValue; + foreach (var item in collection) + { + result = execute(item, temp); + temp = result; + } + + return result; + } + + /// + /// Reduces a sequence to a single accumulated value by repeatedly applying a reducer function + /// that also receives the current element's zero-based index. + /// + /// + /// The sequence to reduce. + /// If or empty, is ignored and + /// () is returned. + /// + /// + /// The reducer function. Receives the current element, the current accumulated value, + /// and the zero-based index of the current element; returns the new accumulated value. + /// + /// + /// The starting value for the accumulator before the first element is processed. + /// Defaults to (). + /// + /// The element type of the input sequence. + /// The type of the accumulated result. + /// The final accumulated value after all elements have been processed. + /// + /// + /// // Build indexed labels + /// new[] { "apple", "banana", "cherry" } + /// .Reduce((item, acc, index) => acc + $"{index}: {item}\n", "") + /// // "0: apple\n1: banana\n2: cherry\n" + /// + /// + public static TOut Reduce(this IEnumerable values, Func execute, TOut initialValue = default) + { + var collection = values?.ToList() ?? new List(); + var result = default(TOut); + var temp = initialValue; + for (int counter = 0; counter <= collection.Count - 1; counter++) + { + var item = collection[counter]; + result = execute(item, temp, counter); + temp = result; + } + + return result; + } +} + diff --git a/src/CSharpHelperExtensions/GenericExtensions.cs b/src/CSharpHelperExtensions/GenericExtensions.cs new file mode 100644 index 0000000..25ae732 --- /dev/null +++ b/src/CSharpHelperExtensions/GenericExtensions.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using Newtonsoft.Json; + +namespace CSharpHelperExtensions; + +/// +/// Controls which bounds are included when using . +/// +public enum BetweenComparison +{ + /// Inclusive on both ends: lower ≤ value ≤ upper. This is the default. + None, + /// Exclusive on both ends: lower < value < upper. + ExcludeBoth, + /// Exclusive lower bound, inclusive upper: lower < value ≤ upper. + ExcludeLower, + /// Inclusive lower bound, exclusive upper: lower ≤ value < upper. + ExcludeUpper +} +public static class GenericExtensions +{ + + /// + /// Determines whether a value falls within the range defined by and . + /// + /// The value to test. + /// The lower bound of the range. + /// The upper bound of the range. + /// + /// Controls which bounds are included in the comparison. + /// Defaults to , which is inclusive on both ends (lower ≤ value ≤ upper). + /// + /// Any type that implements . + /// + /// if satisfies the range check; + /// otherwise . + /// + /// + /// + /// 5.IsBetween(1, 10) // true (inclusive both ends) + /// 1.IsBetween(1, 10) // true (lower bound included) + /// 1.IsBetween(1, 10, BetweenComparison.ExcludeLower) // false (lower bound excluded) + /// 10.IsBetween(1, 10, BetweenComparison.ExcludeUpper) // false (upper bound excluded) + /// 10.IsBetween(1, 10, BetweenComparison.ExcludeBoth) // false (both bounds excluded) + /// + /// + public static bool IsBetween(this T value, T lower, T upper, BetweenComparison comparison = BetweenComparison.None) + where T : IComparable + { + return comparison switch + { + BetweenComparison.ExcludeBoth => (value.CompareTo(lower) > 0) && (value.CompareTo(upper) < 0), + BetweenComparison.ExcludeLower => (value.CompareTo(lower) > 0) && (value.CompareTo(upper) <= 0), + BetweenComparison.ExcludeUpper => (value.CompareTo(lower) >= 0) && (value.CompareTo(upper) < 0), + _ => (lower.CompareTo(value) <= 0) && (value.CompareTo(upper) <= 0) + }; + } + + /// + /// Determines whether a value equals any item in a given set. + /// Equivalent to SQL's IN operator. + /// + /// The value to look for. + /// One or more candidate values to match against. + /// The type of the value and candidates. + /// + /// Returns when no candidates are supplied. + /// Equality is determined by the default equality comparer for . + /// + /// + /// if matches any item in ; + /// otherwise . + /// + /// + /// + /// "admin".In("admin", "superadmin") // true + /// "guest".In("admin", "superadmin") // false + /// 3.In(1, 2, 3, 4) // true + /// + /// + public static bool In(this T value, params T[] input) + { + return input is { } && input.Contains(value); + } + + /// + /// Returns if the string is , empty, + /// or consists only of whitespace characters. + /// Delegates to . + /// + /// + /// Despite the name, whitespace-only strings are treated as empty (uses internally, + /// not ). + /// This overload operates on . + /// For collections, use + /// + /// (requires the CSharpHelperExtensions.Enumerable namespace). + /// + /// The string to check. + /// + /// if is , empty, or whitespace-only; + /// otherwise . + /// + /// + /// + /// ((string)null).IsNullOrEmpty() // true + /// "".IsNullOrEmpty() // true + /// " ".IsNullOrEmpty() // true (whitespace only) + /// "hello".IsNullOrEmpty() // false + /// + /// + public static bool IsNullOrEmpty(this string value) + { + return string.IsNullOrWhiteSpace(value); + } + + /// + /// Serializes an object to its JSON string representation using Newtonsoft.Json. + /// + /// + /// The object to serialize. Returns when this argument is . + /// + /// + /// When , the JSON output is pretty-printed with indentation. + /// Defaults to (compact, single-line output). + /// + /// + /// The type of the object to serialize. Must be a reference type (class). + /// Value types (structs) must be boxed to before calling this method. + /// + /// + /// A JSON string representing , + /// or if is . + /// + /// + /// + /// var obj = new { Name = "Alice", Age = 30 }; + /// obj.ToJson() // {"Name":"Alice","Age":30} + /// obj.ToJson(indentation: true) + /// // { + /// // "Name": "Alice", + /// // "Age": 30 + /// // } + /// ((object)null).ToJson() // null + /// + /// + public static string ToJson(this T value, bool indentation = false) where T : class + { + var formatting = indentation ? Formatting.Indented : Formatting.None; + return value == null ? null : JsonConvert.SerializeObject(value, formatting); + } +} \ No newline at end of file diff --git a/src/CSharpHelperExtensions/StringExtensions.cs b/src/CSharpHelperExtensions/StringExtensions.cs new file mode 100644 index 0000000..d889c41 --- /dev/null +++ b/src/CSharpHelperExtensions/StringExtensions.cs @@ -0,0 +1,55 @@ +using System; +using System.ComponentModel; + +namespace CSharpHelperExtensions.Strings; + +public static class StringExtensions +{ + /// + /// Converts a string to the specified nullable value type using its registered . + /// Returns when the input is , empty, or whitespace. + /// + /// The string to convert. Accepts . + /// + /// The target value type (e.g. , , ). + /// Must be a struct; the return type is ?. + /// + /// + /// The converted value wrapped in a nullable, + /// or if is , empty, or whitespace. + /// + /// + /// Thrown when is not in a valid format for . + /// + /// + /// Thrown when the for does not support + /// conversion from a string. + /// + /// + /// + /// "42".ToNullable<int>() // (int?)42 + /// "".ToNullable<int>() // null + /// " ".ToNullable<int>() // null (whitespace) + /// "2024-01-15".ToNullable<DateTime>() // parsed DateTime (result is culture-dependent) + /// "not-a-number".ToNullable<int>() // throws FormatException + /// + /// + public static T? ToNullable(this string input) where T : struct + { + try + { + if (input.IsNullOrEmpty()) + { + return null; + } + + var conv = TypeDescriptor.GetConverter(typeof(T)); + return (T?)conv.ConvertFrom(input); + } + catch (Exception e) + { + throw; + } + } +} +