Skip to content

Commit 0b4f384

Browse files
authored
refactor!: Polyfill: Add universal ThrowIf* support for all .NET (#470)
* refactor!: Polyfill: Add universal ThrowIf* support for all .NET Refactor to provide polyfills for modern argument validation APIs (ArgumentNullException.ThrowIfNull, ArgumentException.ThrowIfNullOrEmpty/WhiteSpace, ArgumentOutOfRangeException.ThrowIf*) across all supported .NET targets, including .NET Standard 2.0 and .NET Framework. Mark legacy Argument.ThrowIf* methods as obsolete and delegate to new polyfills. Expand target frameworks, update CI for matrix builds, and add comprehensive unit tests. Remove legacy helpers and update package metadata to reflect universal polyfill focus. * ci: Disabled `failFast` * docs: Updated README * fix: Switched to `windows-2022` * fix: Corrected RuntimeIdentifier * fix: Fixed `RuntimeIdentifier` * docs: Updated XML Summaries * fix: Updated `packagetags` * chore: Try Run on `windows-latest`
1 parent 6d84c5c commit 0b4f384

34 files changed

+1546
-317
lines changed

.editorconfig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ generated_code = true
4444
# XML project files
4545
[*.{slnx,csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj,nativeproj,locproj}]
4646
indent_size = 2
47+
max_line_length = 160
4748

4849
# Xml build files
4950
[*.builds]

.github/workflows/cicd.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,6 @@ jobs:
2525
with:
2626
dotnetLogging: ${{ inputs.dotnet-logging }}
2727
dotnetVersion: ${{ vars.NE_DOTNET_TARGETFRAMEWORKS }}
28+
runsOnBuild: windows-latest
2829
solution: ./Arguments.slnx
2930
secrets: inherit

Directory.Build.props

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
<Project>
22
<PropertyGroup>
3-
<!-- Workaround, until https://github.com/GitTools/GitVersion/pull/4206 is released -->
4-
<GitVersionTargetFramework>net8.0</GitVersionTargetFramework>
3+
<_NetTargetFrameworks>net6.0;net7.0;net8.0;net9.0;net10.0</_NetTargetFrameworks>
4+
<_ClassicTargetFrameworks>net472;net48;net481</_ClassicTargetFrameworks>
5+
<_ProjectTargetFrameworks>$(_NetTargetFrameworks);netstandard2.0</_ProjectTargetFrameworks>
6+
<_ProjectTargetFrameworks Condition=" '$(OS)' == 'Windows_NT' "
7+
>$(_ProjectTargetFrameworks);$(_ClassicTargetFrameworks)</_ProjectTargetFrameworks
8+
>
9+
<_TestTargetFrameworks>$(_NetTargetFrameworks)</_TestTargetFrameworks>
10+
<_TestTargetFrameworks Condition=" '$(OS)' == 'Windows_NT' "
11+
>$(_TestTargetFrameworks);$(_ClassicTargetFrameworks)</_TestTargetFrameworks
12+
>
13+
<CheckEolTargetFramework>false</CheckEolTargetFramework>
514
</PropertyGroup>
615
</Project>

README.md

Lines changed: 136 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,21 @@ A comprehensive library providing backward-compatible argument validation helper
99

1010
Modern .NET versions (starting with .NET 6) introduced streamlined argument validation methods such as `ArgumentNullException.ThrowIfNull` and `ArgumentOutOfRangeException.ThrowIfEqual`. However, projects targeting multiple frameworks or older .NET versions cannot utilize these convenient methods without conditional compilation or duplicated validation logic.
1111

12-
**NetEvolve.Arguments** bridges this gap by providing polyfilled implementations of these modern validation methods, allowing developers to write consistent, maintainable argument validation code regardless of the target framework.
12+
**NetEvolve.Arguments** bridges this gap by providing full polyfill implementations via extension methods on `ArgumentNullException`, `ArgumentException`, and `ArgumentOutOfRangeException`. These polyfills enable the use of modern .NET API patterns across all supported frameworks, allowing developers to write consistent, maintainable argument validation code regardless of the target framework.
13+
14+
### Polyfill Architecture
15+
16+
The library provides polyfills through three main extension classes:
17+
18+
- **`ArgumentNullExceptionPolyfills`**: Extends `ArgumentNullException` with `ThrowIfNull` methods
19+
- **`ArgumentExceptionPolyfills`**: Extends `ArgumentException` with `ThrowIfNullOrEmpty` and `ThrowIfNullOrWhiteSpace` methods
20+
- **`ArgumentOutOfRangeExceptionPolyfills`**: Extends `ArgumentOutOfRangeException` with range validation methods (`ThrowIfZero`, `ThrowIfNegative`, `ThrowIfEqual`, comparison methods, etc.)
21+
22+
These polyfills are conditionally compiled and only active when targeting frameworks that don't provide the native implementations, ensuring zero overhead on modern .NET versions.
1323

1424
## Key Features
1525

16-
- **Multi-Framework Support**: Compatible with .NET Standard 2.0, .NET 8.0, .NET 9.0, and .NET 10.0
26+
- **Multi-Framework Support**: Compatible with .NET Standard 2.0, .NET 6.0-10.0, and .NET Framework 4.7.2-4.8.1 (on Windows)
1727
- **Zero Runtime Overhead**: Uses conditional compilation to delegate to native implementations where available
1828
- **Drop-in Replacement**: Identical API signatures to native .NET implementations
1929
- **Type-Safe**: Fully generic implementations with proper type constraints
@@ -35,154 +45,233 @@ Install-Package NetEvolve.Arguments
3545

3646
## Usage
3747

38-
Import the namespace in your code:
39-
40-
```csharp
41-
using NetEvolve.Arguments;
42-
```
43-
44-
Then use the validation methods just as you would with native .NET implementations:
48+
Simply use the validation methods directly on the exception types, just as you would with native .NET 8+ implementations:
4549

4650
```csharp
4751
public void ProcessData(string data, int count)
4852
{
49-
Argument.ThrowIfNullOrWhiteSpace(data);
50-
Argument.ThrowIfLessThan(count, 1);
53+
ArgumentException.ThrowIfNullOrWhiteSpace(data);
54+
ArgumentOutOfRangeException.ThrowIfLessThan(count, 1);
5155

5256
// Your implementation
5357
}
5458
```
5559

60+
The polyfills are automatically available through extension methods when targeting older frameworks. No additional using directives are needed since the polyfills reside in the `System` namespace.
61+
5662
## Available Methods
5763

5864
### Null Validation
5965

60-
#### `Argument.ThrowIfNull(object?, string?)`
66+
#### `ArgumentNullException.ThrowIfNull(object?, string?)`
6167
Throws an `ArgumentNullException` if the argument is `null`.
6268

63-
**Replacement for**: [`ArgumentNullException.ThrowIfNull(object, string)`](https://learn.microsoft.com/en-us/dotnet/api/system.argumentnullexception.throwifnull) (introduced in .NET 6)
69+
**Native API**: [`ArgumentNullException.ThrowIfNull`](https://learn.microsoft.com/en-us/dotnet/api/system.argumentnullexception.throwifnull) (introduced in .NET 6)
70+
71+
**Polyfill availability**: .NET Standard 2.0, .NET Framework 4.7.2-4.8.1
6472

6573
**Example**:
6674
```csharp
6775
public void Process(object data)
6876
{
69-
Argument.ThrowIfNull(data);
77+
ArgumentNullException.ThrowIfNull(data);
7078
}
7179
```
7280

73-
#### `Argument.ThrowIfNull(void*, string?)`
81+
#### `ArgumentNullException.ThrowIfNull(void*, string?)`
7482
Throws an `ArgumentNullException` if the pointer argument is `null`.
7583

76-
**Replacement for**: [`ArgumentNullException.ThrowIfNull(void*, string)`](https://learn.microsoft.com/en-us/dotnet/api/system.argumentnullexception.throwifnull?view=net-8.0#system-argumentnullexception-throwifnull(system-void*-system-string)) (introduced in .NET 7)
84+
**Native API**: [`ArgumentNullException.ThrowIfNull(void*)`](https://learn.microsoft.com/en-us/dotnet/api/system.argumentnullexception.throwifnull?view=net-10.0#system-argumentnullexception-throwifnull(system-void*-system-string)) (introduced in .NET 7)
85+
86+
**Polyfill availability**: .NET Standard 2.0, .NET Framework 4.7.2-4.8.1, .NET 6.0
87+
88+
**Example**:
89+
```csharp
90+
public unsafe void Process(void* pointer)
91+
{
92+
ArgumentNullException.ThrowIfNull(pointer);
93+
}
94+
```
7795

78-
#### `Argument.ThrowIfNullOrEmpty(string?, string?)`
96+
#### `ArgumentException.ThrowIfNullOrEmpty(string?, string?)`
7997
Throws an `ArgumentNullException` if the argument is `null`, or an `ArgumentException` if the argument is an empty string.
8098

81-
**Replacement for**: [`ArgumentException.ThrowIfNullOrEmpty(string, string)`](https://learn.microsoft.com/en-us/dotnet/api/system.argumentexception.throwifnullorempty) (introduced in .NET 7)
99+
**Native API**: [`ArgumentException.ThrowIfNullOrEmpty`](https://learn.microsoft.com/en-us/dotnet/api/system.argumentexception.throwifnullorempty) (introduced in .NET 7)
100+
101+
**Polyfill availability**: .NET Standard 2.0, .NET Framework 4.7.2-4.8.1, .NET 6.0
82102

83103
**Example**:
84104
```csharp
85105
public void Process(string name)
86106
{
87-
Argument.ThrowIfNullOrEmpty(name);
107+
ArgumentException.ThrowIfNullOrEmpty(name);
88108
}
89109
```
90110

91-
#### `Argument.ThrowIfNullOrEmpty<T>(IEnumerable<T>?, string?)`
111+
#### `ArgumentException.ThrowIfNullOrEmpty<T>(IEnumerable<T>?, string?)`
92112
Throws an `ArgumentNullException` if the argument is `null`, or an `ArgumentException` if the collection is empty.
93113

94-
**Note**: This is a custom extension method not present in the base .NET framework, providing convenient collection validation.
114+
**Note**: This is a custom extension method not present in the native .NET framework, providing convenient collection validation.
115+
116+
**Availability**: All supported frameworks
95117

96118
**Example**:
97119
```csharp
98120
public void Process(IEnumerable<int> items)
99121
{
100-
Argument.ThrowIfNullOrEmpty(items);
122+
ArgumentException.ThrowIfNullOrEmpty(items);
101123
}
102124
```
103125

104-
#### `Argument.ThrowIfNullOrWhiteSpace(string?, string?)`
126+
#### `ArgumentException.ThrowIfNullOrWhiteSpace(string?, string?)`
105127
Throws an `ArgumentNullException` if the argument is `null`, or an `ArgumentException` if the argument is empty or contains only white-space characters.
106128

107-
**Replacement for**: [`ArgumentException.ThrowIfNullOrWhiteSpace(string, string)`](https://learn.microsoft.com/en-us/dotnet/api/system.argumentexception.throwifnullorwhitespace) (introduced in .NET 8)
129+
**Native API**: [`ArgumentException.ThrowIfNullOrWhiteSpace`](https://learn.microsoft.com/en-us/dotnet/api/system.argumentexception.throwifnullorwhitespace) (introduced in .NET 8)
130+
131+
**Polyfill availability**: .NET Standard 2.0, .NET Framework 4.7.2-4.8.1, .NET 6.0, .NET 7.0
108132

109133
**Example**:
110134
```csharp
111135
public void Process(string description)
112136
{
113-
Argument.ThrowIfNullOrWhiteSpace(description);
137+
ArgumentException.ThrowIfNullOrWhiteSpace(description);
114138
}
115139
```
116140

117141
### Range Validation
118142

119-
#### `Argument.ThrowIfEqual<T>(T, T, string?)`
143+
#### `ArgumentOutOfRangeException.ThrowIfZero<T>(T, string?)`
144+
Throws an `ArgumentOutOfRangeException` if the argument is zero.
145+
146+
**Native API**: [`ArgumentOutOfRangeException.ThrowIfZero`](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception.throwifzero) (introduced in .NET 8)
147+
148+
**Polyfill availability**: .NET Standard 2.0, .NET Framework 4.7.2-4.8.1, .NET 6.0, .NET 7.0
149+
150+
**Example**:
151+
```csharp
152+
public void SetDivisor(int divisor)
153+
{
154+
ArgumentOutOfRangeException.ThrowIfZero(divisor);
155+
}
156+
```
157+
158+
#### `ArgumentOutOfRangeException.ThrowIfNegative<T>(T, string?)`
159+
Throws an `ArgumentOutOfRangeException` if the argument is negative.
160+
161+
**Native API**: [`ArgumentOutOfRangeException.ThrowIfNegative`](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception.throwifnegative) (introduced in .NET 8)
162+
163+
**Polyfill availability**: .NET Standard 2.0, .NET Framework 4.7.2-4.8.1, .NET 6.0, .NET 7.0
164+
165+
**Example**:
166+
```csharp
167+
public void SetCount(int count)
168+
{
169+
ArgumentOutOfRangeException.ThrowIfNegative(count);
170+
}
171+
```
172+
173+
#### `ArgumentOutOfRangeException.ThrowIfNegativeOrZero<T>(T, string?)`
174+
Throws an `ArgumentOutOfRangeException` if the argument is negative or zero.
175+
176+
**Native API**: [`ArgumentOutOfRangeException.ThrowIfNegativeOrZero`](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception.throwifnegativeorzero) (introduced in .NET 8)
177+
178+
**Polyfill availability**: .NET Standard 2.0, .NET Framework 4.7.2-4.8.1, .NET 6.0, .NET 7.0
179+
180+
**Example**:
181+
```csharp
182+
public void SetQuantity(int quantity)
183+
{
184+
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(quantity);
185+
}
186+
```
187+
188+
#### `ArgumentOutOfRangeException.ThrowIfEqual<T>(T, T, string?)`
120189
Throws an `ArgumentOutOfRangeException` if the first argument is equal to the second argument.
121190

122-
**Replacement for**: [`ArgumentOutOfRangeException.ThrowIfEqual<T>(T, T, string)`](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception.throwifequal) (introduced in .NET 8)
191+
**Native API**: [`ArgumentOutOfRangeException.ThrowIfEqual`](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception.throwifequal) (introduced in .NET 8)
192+
193+
**Polyfill availability**: .NET Standard 2.0, .NET Framework 4.7.2-4.8.1, .NET 6.0, .NET 7.0
123194

124195
**Example**:
125196
```csharp
126197
public void SetValue(int value)
127198
{
128-
Argument.ThrowIfEqual(value, 0); // Value must not be zero
199+
ArgumentOutOfRangeException.ThrowIfEqual(value, 0); // Value must not be zero
129200
}
130201
```
131202

132-
#### `Argument.ThrowIfNotEqual<T>(T, T, string?)`
203+
#### `ArgumentOutOfRangeException.ThrowIfNotEqual<T>(T, T, string?)`
133204
Throws an `ArgumentOutOfRangeException` if the first argument is not equal to the second argument.
134205

135-
**Replacement for**: [`ArgumentOutOfRangeException.ThrowIfNotEqual<T>(T, T, string)`](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception.throwifnotequal) (introduced in .NET 8)
206+
**Native API**: [`ArgumentOutOfRangeException.ThrowIfNotEqual`](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception.throwifnotequal) (introduced in .NET 8)
136207

137-
#### `Argument.ThrowIfGreaterThan<T>(T, T, string?)`
208+
**Polyfill availability**: .NET Standard 2.0, .NET Framework 4.7.2-4.8.1, .NET 6.0, .NET 7.0
209+
210+
**Example**:
211+
```csharp
212+
public void ValidateConstant(int value)
213+
{
214+
ArgumentOutOfRangeException.ThrowIfNotEqual(value, 42); // Value must be exactly 42
215+
}
216+
```
217+
218+
#### `ArgumentOutOfRangeException.ThrowIfGreaterThan<T>(T, T, string?)`
138219
Throws an `ArgumentOutOfRangeException` if the first argument is greater than the second argument.
139220

140-
**Replacement for**: [`ArgumentOutOfRangeException.ThrowIfGreaterThan<T>(T, T, string)`](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception.throwifgreaterthan) (introduced in .NET 8)
221+
**Native API**: [`ArgumentOutOfRangeException.ThrowIfGreaterThan`](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception.throwifgreaterthan) (introduced in .NET 8)
222+
223+
**Polyfill availability**: .NET Standard 2.0, .NET Framework 4.7.2-4.8.1, .NET 6.0, .NET 7.0
141224

142225
**Example**:
143226
```csharp
144227
public void SetAge(int age)
145228
{
146-
Argument.ThrowIfGreaterThan(age, 150); // Age must be 150 or less
229+
ArgumentOutOfRangeException.ThrowIfGreaterThan(age, 150); // Age must be 150 or less
147230
}
148231
```
149232

150-
#### `Argument.ThrowIfGreaterThanOrEqual<T>(T, T, string?)`
233+
#### `ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual<T>(T, T, string?)`
151234
Throws an `ArgumentOutOfRangeException` if the first argument is greater than or equal to the second argument.
152235

153-
**Replacement for**: [`ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual<T>(T, T, string)`](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception.throwifgreaterthanorequal) (introduced in .NET 8)
236+
**Native API**: [`ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual`](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception.throwifgreaterthanorequal) (introduced in .NET 8)
237+
238+
**Polyfill availability**: .NET Standard 2.0, .NET Framework 4.7.2-4.8.1, .NET 6.0, .NET 7.0
154239

155240
**Example**:
156241
```csharp
157242
public void SetCount(int count, int maximum)
158243
{
159-
Argument.ThrowIfGreaterThanOrEqual(count, maximum); // Count must be less than maximum
244+
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(count, maximum); // Count must be less than maximum
160245
}
161246
```
162247

163-
#### `Argument.ThrowIfLessThan<T>(T, T, string?)`
248+
#### `ArgumentOutOfRangeException.ThrowIfLessThan<T>(T, T, string?)`
164249
Throws an `ArgumentOutOfRangeException` if the first argument is less than the second argument.
165250

166-
**Replacement for**: [`ArgumentOutOfRangeException.ThrowIfLessThan<T>(T, T, string)`](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception.throwiflessthan) (introduced in .NET 8)
251+
**Native API**: [`ArgumentOutOfRangeException.ThrowIfLessThan`](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception.throwiflessthan) (introduced in .NET 8)
252+
253+
**Polyfill availability**: .NET Standard 2.0, .NET Framework 4.7.2-4.8.1, .NET 6.0, .NET 7.0
167254

168255
**Example**:
169256
```csharp
170257
public void SetCount(int count)
171258
{
172-
Argument.ThrowIfLessThan(count, 1); // Count must be at least 1
259+
ArgumentOutOfRangeException.ThrowIfLessThan(count, 1); // Count must be at least 1
173260
}
174261
```
175262

176-
#### `Argument.ThrowIfLessThanOrEqual<T>(T, T, string?)`
263+
#### `ArgumentOutOfRangeException.ThrowIfLessThanOrEqual<T>(T, T, string?)`
177264
Throws an `ArgumentOutOfRangeException` if the first argument is less than or equal to the second argument.
178265

179-
**Replacement for**: [`ArgumentOutOfRangeException.ThrowIfLessThanOrEqual<T>(T, T, string)`](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception.throwiflessthanorequal) (introduced in .NET 8)
266+
**Native API**: [`ArgumentOutOfRangeException.ThrowIfLessThanOrEqual`](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception.throwiflessthanorequal) (introduced in .NET 8)
267+
268+
**Polyfill availability**: .NET Standard 2.0, .NET Framework 4.7.2-4.8.1, .NET 6.0, .NET 7.0
180269

181270
**Example**:
182271
```csharp
183272
public void SetMinimum(int value, int threshold)
184273
{
185-
Argument.ThrowIfLessThanOrEqual(value, threshold); // Value must be greater than threshold
274+
ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(value, threshold); // Value must be greater than threshold
186275
}
187276
```
188277

@@ -191,6 +280,11 @@ public void SetMinimum(int value, int threshold)
191280
| Target Framework | Status | Notes |
192281
|-----------------|--------|-------|
193282
| .NET Standard 2.0 | ✅ Supported | Full polyfill implementations |
283+
| .NET Framework 4.7.2 | ✅ Supported | Windows only, full polyfill implementations |
284+
| .NET Framework 4.8 | ✅ Supported | Windows only, full polyfill implementations |
285+
| .NET Framework 4.8.1 | ✅ Supported | Windows only, full polyfill implementations |
286+
| .NET 6.0 | ✅ Supported | Delegates to native implementations where available |
287+
| .NET 7.0 | ✅ Supported | Delegates to native implementations where available |
194288
| .NET 8.0 | ✅ Supported | Delegates to native implementations where available |
195289
| .NET 9.0 | ✅ Supported | Full native delegation |
196290
| .NET 10.0 | ✅ Supported | Full native delegation |

src/NetEvolve.Arguments/Argument.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55
/// <summary>
66
/// Provides a set of backward compatible `throw` helper methods, which have been added in previous .NET versions.
77
/// </summary>
8-
[SuppressMessage("Style", "IDE0022:Use expression body for method", Justification = "As designed.")]
9-
public static partial class Argument { }
8+
[SuppressMessage("Info Code Smell", "S1133:Deprecated code should be removed", Justification = "As designed.")]
9+
public static partial class Argument;

0 commit comments

Comments
 (0)