Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<Version>15.0.0</Version>
<Version>15.1.0</Version>
<LangVersion>14</LangVersion>
<Authors>Kenny Pflug</Authors>
<Company>Kenny Pflug</Company>
Expand Down
55 changes: 55 additions & 0 deletions Light.GuardClauses.SingleFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10134,6 +10134,55 @@ public static string MustStartWith([NotNull, ValidatedNotNull] this string? para

return parameter;
}

/// <summary>
/// Checks if the specified <paramref name = "condition"/> is true and throws an <see cref = "ObjectDisposedException"/> in this case.
/// </summary>
/// <param name = "condition">The condition to be checked. The exception is thrown when it is true.</param>
/// <param name = "objectName">The name of the disposed object (optional).</param>
/// <param name = "message">The message that will be passed to the <see cref = "ObjectDisposedException"/> (optional).</param>
/// <exception cref = "ObjectDisposedException">Thrown when <paramref name = "condition"/> is true.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ObjectDisposed(bool condition, string? objectName = null, string? message = null)
{
if (condition)
{
Throw.ObjectDisposed(objectName, message);
}
}

/// <summary>
/// Checks if the specified <paramref name = "condition"/> is true and throws your custom exception in this case.
/// </summary>
/// <param name = "condition">The condition to be checked. The exception is thrown when it is true.</param>
/// <param name = "exceptionFactory">The delegate that creates your custom exception.</param>
/// <exception cref = "Exception">Your custom exception thrown when <paramref name = "condition"/> is true.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[ContractAnnotation("exceptionFactory:null => halt")]
public static void ObjectDisposed(bool condition, Func<Exception> exceptionFactory)
{
if (condition)
{
Throw.CustomException(exceptionFactory);
}
}

/// <summary>
/// Checks if the specified <paramref name = "condition"/> is true and throws your custom exception in this case.
/// </summary>
/// <param name = "condition">The condition to be checked. The exception is thrown when it is true.</param>
/// <param name = "parameter">The value that is checked in the <paramref name = "condition"/>. This value is passed to the <paramref name = "exceptionFactory"/>.</param>
/// <param name = "exceptionFactory">The delegate that creates your custom exception. The <paramref name = "parameter"/> is passed to this delegate.</param>
/// <exception cref = "Exception">Your custom exception thrown when <paramref name = "condition"/> is true.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[ContractAnnotation("exceptionFactory:null => halt")]
public static void ObjectDisposed<T>(bool condition, T parameter, Func<T, Exception> exceptionFactory)
{
if (condition)
{
Throw.CustomException(exceptionFactory, parameter);
}
}
}

/// <summary>
Expand Down Expand Up @@ -11839,6 +11888,12 @@ public static void MustNotBeLessThanOrEqualTo<T>(T parameter, T boundary, [Calle
[DoesNotReturn]
public static void NullableHasNoValue(string? parameterName = null, string? message = null) => throw new NullableHasNoValueException(parameterName, message ?? $"{parameterName ?? "The nullable"} must have a value, but it actually is null.");
/// <summary>
/// Throws an <see cref = "ObjectDisposedException"/> using the optional object name and message.
/// </summary>
[ContractAnnotation("=> halt")]
[DoesNotReturn]
public static void ObjectDisposed(string? objectName = null, string? message = null) => throw new ObjectDisposedException(objectName, message);
/// <summary>
/// Throws the default <see cref = "ArgumentOutOfRangeException"/> indicating that a value is not within a specified
/// range, using the optional parameter name and message.
/// </summary>
Expand Down
33 changes: 33 additions & 0 deletions ai-plans/0163-object-disposed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Object-Disposed Condition Guard

## Rationale

Parent issue #162 identifies disposal guards as a recurring need (five call sites in BrilliantMessaging alone) that currently must be expressed as a generic boolean assertion with an exception factory, obscuring intent. Add `Check.ObjectDisposed` as a condition guard analogous to `Check.InvalidArgument`/`Check.InvalidOperation` that throws `ObjectDisposedException` when the condition is true, so disposal checks read explicitly and remain usable on `netstandard2.0`.

## Acceptance Criteria

- [x] `Check.ObjectDisposed(condition, objectName, message)` throws `ObjectDisposedException` only when `condition` is true, identically on .NET Standard 2.0, .NET Standard 2.1, and .NET 10; the supplied object name is exposed via `ObjectName`, the optional message via `Message`, and omitted arguments fall back to the BCL default message.
- [x] Two custom-exception-factory overloads follow the `InvalidArgument` convention (plain `Func<Exception>` and generic `Func<T, Exception>` receiving a caller-supplied parameter); they throw the factory's exception only when the condition is true, never invoke the factory otherwise, and a null factory on a true condition throws `ArgumentNullException` via the existing `Throw.CustomException` convention.
- [x] Automated tests cover: condition true/false, object-name and message propagation, the BCL default message, both factory overloads (including concrete custom exception instances and the passed parameter), factories not invoked on a false condition, and null-factory behavior.
- [x] The source-export whitelist catalog and committed settings contain `ObjectDisposed`, and focused source-export tests cover retention of the guard and its `Throw.ObjectDisposed` helper as well as trimming of the exception-factory overloads when configured.
- [x] The committed .NET Standard 2.0 single-file distribution is regenerated with the new guard and validates for both supported source-export targets.
- [x] The "Condition and state assertions" table in `docs/assertion-overview.md` lists `ObjectDisposed`, and the 15.0.0 package release notes in `src/Directory.Build.props` mention the new guard.
- [x] The complete solution restores and builds without warnings in Release configuration, and all automated tests pass on the pinned SDK.

## Technical Details

Add the guard as a new `Check.ObjectDisposed.cs` modeled directly on `Check.InvalidArgument.cs` (aggressive inlining, JetBrains contract annotations, XML documentation style), plus a non-returning `Throw.ObjectDisposed` helper in `ExceptionFactory/Throw.ObjectDisposed.cs` modeled on `Throw.InvalidOperation.cs` (`[ContractAnnotation("=> halt")]`, `[DoesNotReturn]`) whose single expression throws `new ObjectDisposedException(objectName, message)`. The exact public shape is:

```csharp
public static void ObjectDisposed(bool condition, string? objectName = null, string? message = null);

public static void ObjectDisposed(bool condition, Func<Exception> exceptionFactory);

public static void ObjectDisposed<T>(bool condition, T parameter, Func<T, Exception> exceptionFactory);
```

The generic factory overload mirrors `InvalidArgument`'s `Func<T, Exception>` form: callers pass arbitrary failure context (typically the disposed object or its name) without closure allocation. No caller-argument-expression capture applies because condition guards take a pre-evaluated `bool`, matching the other condition checks.

`ObjectDisposedException(string, string)` exists on all library targets and supplies the "Cannot access a disposed object." default when the message is null, so no custom exception type or message composition is introduced. No microbenchmarks: the guard is a trivial inlined condition check like `InvalidOperation`.

Append an `ObjectDisposed` entry to `AssertionWhitelist` and `settings.json` (after `MustStartWith`, preserving alphabetical position), add focused facts to `SourceFileMergerWhitelistTests` for dependency retention and factory-overload trimming, add `ObjectDisposedTests` under `tests/Light.GuardClauses.Tests/CommonAssertions/` following `InvalidOperationTests`/`InvalidArgumentTests` conventions (FluentAssertions, `DefaultVariablesData` where useful), and regenerate `Light.GuardClauses.SingleFile.cs` via the source-export project's committed settings.
3 changes: 2 additions & 1 deletion docs/assertion-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

This page groups the current public assertion families defined by the `Check.*.cs` files. Exact overloads, generic constraints, return annotations, and exception contracts are documented in the source XML comments and appear in IntelliSense.

Names beginning with `Must` validate or throw and return the successfully validated value. `InvalidArgument`, `InvalidOperation`, and `InvalidState` are throwing condition checks. Other names return `bool`. Most throwing families offer a default-exception overload and an exception-factory overload; see [Structuring precondition checks](structuring-precondition-checks.md).
Names beginning with `Must` validate or throw and return the successfully validated value. `InvalidArgument`, `InvalidOperation`, `InvalidState`, and `ObjectDisposed` are throwing condition checks. Other names return `bool`. Most throwing families offer a default-exception overload and an exception-factory overload; see [Structuring precondition checks](structuring-precondition-checks.md).

## Target-specific API

Expand Down Expand Up @@ -52,6 +52,7 @@ UUIDv7 validation checks the version-7 nibble and RFC/IETF `10xx` variant bits d
| `InvalidArgument` | Throws `ArgumentException` when the condition is true |
| `InvalidOperation` | Throws `InvalidOperationException` when the condition is true |
| `InvalidState` | Throws `InvalidStateException` when the condition is true |
| `ObjectDisposed` | Throws `ObjectDisposedException` when the condition is true |

## Comparable, range, and approximate assertions

Expand Down
12 changes: 2 additions & 10 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,10 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageReleaseNotes>
Light.GuardClauses 15.0.0
Light.GuardClauses 15.1.0
--------------------------------

- new assertions: IsUuidVersion7, MustBeUuidVersion7, IsFinite, MustBeFinite, IsAscii, MustBeAscii, and MustHaveCountIn
- new numeric sign guards: MustBePositive, MustBeNegative, MustNotBePositive, MustNotBeNegative, and MustNotBeZero
- new dictionary key guards: MustContainKey and MustNotContainKey
- new collection content guards: MustNotContainNull and MustNotContainNullOrWhiteSpace
- new string inspection assertions: ContainsOnlyDigits, MustContainOnlyDigits, ContainsOnlyLettersOrDigits, MustContainOnlyLettersOrDigits, IsUpperCase, MustBeUpperCase, IsLowerCase, MustBeLowerCase, IsBase64, MustBeBase64, IsHexadecimal, MustBeHexadecimal, and MustNotContainWhiteSpace
- new stream capability guards: MustBeReadable, MustBeWritable, and MustBeSeekable
- new collection count guard: MustHaveSameCountAs
- expanded span and memory support for MustNotBeEmpty, MustHaveLength, MustHaveLengthIn, and MustNotBeEmptyOrWhiteSpace, plus DateTimeOffset support for MustBeUtc
- breaking: the modern package target was updated from .NET 8 to .NET 10; .NET Standard 2.0 and 2.1 remain supported
- new assertions: ObjectDisposed
</PackageReleaseNotes>
</PropertyGroup>

Expand Down
58 changes: 58 additions & 0 deletions src/Light.GuardClauses/Check.ObjectDisposed.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System;
using System.Runtime.CompilerServices;
using JetBrains.Annotations;
using Light.GuardClauses.ExceptionFactory;

namespace Light.GuardClauses;

public static partial class Check
{
/// <summary>
/// Checks if the specified <paramref name="condition" /> is true and throws an <see cref="ObjectDisposedException" /> in this case.
/// </summary>
/// <param name="condition">The condition to be checked. The exception is thrown when it is true.</param>
/// <param name="objectName">The name of the disposed object (optional).</param>
/// <param name="message">The message that will be passed to the <see cref="ObjectDisposedException" /> (optional).</param>
/// <exception cref="ObjectDisposedException">Thrown when <paramref name="condition" /> is true.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ObjectDisposed(bool condition, string? objectName = null, string? message = null)
{
if (condition)
{
Throw.ObjectDisposed(objectName, message);
}
}

/// <summary>
/// Checks if the specified <paramref name="condition" /> is true and throws your custom exception in this case.
/// </summary>
/// <param name="condition">The condition to be checked. The exception is thrown when it is true.</param>
/// <param name="exceptionFactory">The delegate that creates your custom exception.</param>
/// <exception cref="Exception">Your custom exception thrown when <paramref name="condition" /> is true.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[ContractAnnotation("exceptionFactory:null => halt")]
public static void ObjectDisposed(bool condition, Func<Exception> exceptionFactory)
{
if (condition)
{
Throw.CustomException(exceptionFactory);
}
}

/// <summary>
/// Checks if the specified <paramref name="condition" /> is true and throws your custom exception in this case.
/// </summary>
/// <param name="condition">The condition to be checked. The exception is thrown when it is true.</param>
/// <param name="parameter">The value that is checked in the <paramref name="condition" />. This value is passed to the <paramref name="exceptionFactory" />.</param>
/// <param name="exceptionFactory">The delegate that creates your custom exception. The <paramref name="parameter" /> is passed to this delegate.</param>
/// <exception cref="Exception">Your custom exception thrown when <paramref name="condition" /> is true.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[ContractAnnotation("exceptionFactory:null => halt")]
public static void ObjectDisposed<T>(bool condition, T parameter, Func<T, Exception> exceptionFactory)
{
if (condition)
{
Throw.CustomException(exceptionFactory, parameter);
}
}
}
16 changes: 16 additions & 0 deletions src/Light.GuardClauses/ExceptionFactory/Throw.ObjectDisposed.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System;
using System.Diagnostics.CodeAnalysis;
using JetBrains.Annotations;

namespace Light.GuardClauses.ExceptionFactory;

public static partial class Throw
{
/// <summary>
/// Throws an <see cref="ObjectDisposedException" /> using the optional object name and message.
/// </summary>
[ContractAnnotation("=> halt")]
[DoesNotReturn]
public static void ObjectDisposed(string? objectName = null, string? message = null) =>
throw new ObjectDisposedException(objectName, message);
}
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,51 @@ public static void StreamGuardWhitelistTrimsFactoryAndUnrelatedStreamThrowHelper
sourceCode.Should().NotContain("public static void MustBeSeekable(");
}

[Fact]
public static void ObjectDisposedWhitelistExportsGuardThrowHelperAndExceptionFactories()
{
using var temporaryDirectory = new TemporaryDirectory();
var targetFile = Path.Combine(temporaryDirectory.DirectoryPath, "ObjectDisposed.cs");

SourceFileMerger.CreateSingleSourceFile(
CreateOptions(
targetFile,
CreateWhitelist(
includedAssertions: [new ("ObjectDisposed", true)]
)
)
);
var sourceCode = File.ReadAllText(targetFile);

sourceCode.Should().Contain("public static void ObjectDisposed(bool condition, string? objectName = null, string? message = null)");
sourceCode.Should().Contain("public static void ObjectDisposed(bool condition, Func<Exception> exceptionFactory)");
sourceCode.Should().Contain("public static void ObjectDisposed<T>(bool condition, T parameter, Func<T, Exception> exceptionFactory)");
sourceCode.Should().Contain("public static void ObjectDisposed(string? objectName = null, string? message = null) => throw new ObjectDisposedException(objectName, message);");
sourceCode.Should().Contain("public static void CustomException(Func<Exception> exceptionFactory)");
sourceCode.Should().NotContain("public static void InvalidOperation(");
}

[Fact]
public static void ObjectDisposedWhitelistTrimsExceptionFactoryOverloads()
{
using var temporaryDirectory = new TemporaryDirectory();
var targetFile = Path.Combine(temporaryDirectory.DirectoryPath, "ObjectDisposedWithoutFactories.cs");

SourceFileMerger.CreateSingleSourceFile(
CreateOptions(
targetFile,
CreateWhitelist(includedAssertions: [new ("ObjectDisposed", false)])
)
);
var sourceCode = File.ReadAllText(targetFile);

sourceCode.Should().Contain("public static void ObjectDisposed(bool condition, string? objectName = null, string? message = null)");
sourceCode.Should().Contain("public static void ObjectDisposed(string? objectName = null, string? message = null) => throw new ObjectDisposedException(objectName, message);");
sourceCode.Should().NotContain("Func<Exception> exceptionFactory");
sourceCode.Should().NotContain("ObjectDisposed<T>");
sourceCode.Should().NotContain("public static void CustomException(");
}

private static SourceFileMergeOptions CreateOptions(
string targetFile,
AssertionWhitelist assertionWhitelist = null,
Expand Down
Loading
Loading