diff --git a/AGENTS.md b/AGENTS.md index cf25ed3..df13986 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,5 +13,3 @@ Read ./ai-plans/AGENTS.md for details on how to write plans. ## Here is Your Space If you encounter something worth noting while you are working on this code base, write it down here in this section. Once you are finished, I will discuss it with you, and we can decide where to put your notes. - -- The source exporter used to merge `Check.*.cs` files in `DirectoryInfo.GetFiles` order, which is filesystem-dependent and unordered on APFS, so adding source files could reshuffle the entire generated single file. `SourceFileMerger` now sorts the file list with `OrderBy(f => f.Name, StringComparer.Ordinal)`, making the output deterministic. The regeneration that shipped with the sign guards contains the resulting one-time reshuffle; future diffs will only show actual content changes. diff --git a/Light.GuardClauses.SingleFile.cs b/Light.GuardClauses.SingleFile.cs index 9d1d245..fa8760b 100644 --- a/Light.GuardClauses.SingleFile.cs +++ b/Light.GuardClauses.SingleFile.cs @@ -4263,6 +4263,96 @@ public static ImmutableArray MustContain(this ImmutableArray parameter, return parameter; } + /// + /// Ensures that the dictionary contains the specified key, or otherwise throws a . + /// The check is performed via , so the dictionary is + /// never enumerated and its key comparer is respected. + /// + /// The dictionary to be checked. + /// The key that must be present in the dictionary. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// Thrown when does not contain . + /// Thrown when is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] + public static IReadOnlyDictionary MustContainKey([NotNull][ValidatedNotNull] this IReadOnlyDictionary? parameter, TKey key, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + if (!parameter.MustNotBeNull(parameterName, message).ContainsKey(key)) + { + Throw.MissingKey(parameter, key, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the dictionary contains the specified key, or otherwise throws your custom exception. + /// The check is performed via , so the dictionary is + /// never enumerated and its key comparer is respected. + /// + /// The dictionary to be checked. + /// The key that must be present in the dictionary. + /// The delegate that creates your custom exception. and are passed to this delegate. + /// Your custom exception thrown when does not contain , or when is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] + public static IReadOnlyDictionary MustContainKey([NotNull][ValidatedNotNull] this IReadOnlyDictionary? parameter, TKey key, Func?, TKey, Exception> exceptionFactory) + { + if (parameter is null || !parameter.ContainsKey(key)) + { + Throw.CustomException(exceptionFactory, parameter, key); + } + + return parameter; + } + + /// + /// Ensures that the dictionary contains the specified key, or otherwise throws a . + /// The check is performed via , so the dictionary is + /// never enumerated and its key comparer is respected. + /// + /// The dictionary to be checked. + /// The key that must be present in the dictionary. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// Thrown when does not contain . + /// Thrown when is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] + public static Dictionary MustContainKey([NotNull][ValidatedNotNull] this Dictionary? parameter, TKey key, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + where TKey : notnull + { + if (!parameter.MustNotBeNull(parameterName, message).ContainsKey(key)) + { + Throw.MissingKey(parameter, key, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the dictionary contains the specified key, or otherwise throws your custom exception. + /// The check is performed via , so the dictionary is + /// never enumerated and its key comparer is respected. + /// + /// The dictionary to be checked. + /// The key that must be present in the dictionary. + /// The delegate that creates your custom exception. and are passed to this delegate. + /// Your custom exception thrown when does not contain , or when is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] + public static Dictionary MustContainKey([NotNull][ValidatedNotNull] this Dictionary? parameter, TKey key, Func?, TKey, Exception> exceptionFactory) + where TKey : notnull + { + if (parameter is null || !parameter.ContainsKey(key)) + { + Throw.CustomException(exceptionFactory, parameter, key); + } + + return parameter; + } + /// /// Ensures that the string ends with the specified value, or otherwise throws a . /// @@ -7428,6 +7518,96 @@ public static ImmutableArray MustNotContain(this ImmutableArray paramet return parameter; } + /// + /// Ensures that the dictionary does not contain the specified key, or otherwise throws an . + /// The check is performed via , so the dictionary is + /// never enumerated and its key comparer is respected. + /// + /// The dictionary to be checked. + /// The key that must not be present in the dictionary. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// Thrown when contains . + /// Thrown when is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] + public static IReadOnlyDictionary MustNotContainKey([NotNull][ValidatedNotNull] this IReadOnlyDictionary? parameter, TKey key, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + if (parameter.MustNotBeNull(parameterName, message).ContainsKey(key)) + { + Throw.ExistingKey(parameter, key, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the dictionary does not contain the specified key, or otherwise throws your custom exception. + /// The check is performed via , so the dictionary is + /// never enumerated and its key comparer is respected. + /// + /// The dictionary to be checked. + /// The key that must not be present in the dictionary. + /// The delegate that creates your custom exception. and are passed to this delegate. + /// Your custom exception thrown when contains , or when is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] + public static IReadOnlyDictionary MustNotContainKey([NotNull][ValidatedNotNull] this IReadOnlyDictionary? parameter, TKey key, Func?, TKey, Exception> exceptionFactory) + { + if (parameter is null || parameter.ContainsKey(key)) + { + Throw.CustomException(exceptionFactory, parameter, key); + } + + return parameter; + } + + /// + /// Ensures that the dictionary does not contain the specified key, or otherwise throws an . + /// The check is performed via , so the dictionary is + /// never enumerated and its key comparer is respected. + /// + /// The dictionary to be checked. + /// The key that must not be present in the dictionary. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// Thrown when contains . + /// Thrown when is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] + public static Dictionary MustNotContainKey([NotNull][ValidatedNotNull] this Dictionary? parameter, TKey key, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + where TKey : notnull + { + if (parameter.MustNotBeNull(parameterName, message).ContainsKey(key)) + { + Throw.ExistingKey(parameter, key, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the dictionary does not contain the specified key, or otherwise throws your custom exception. + /// The check is performed via , so the dictionary is + /// never enumerated and its key comparer is respected. + /// + /// The dictionary to be checked. + /// The key that must not be present in the dictionary. + /// The delegate that creates your custom exception. and are passed to this delegate. + /// Your custom exception thrown when contains , or when is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] + public static Dictionary MustNotContainKey([NotNull][ValidatedNotNull] this Dictionary? parameter, TKey key, Func?, TKey, Exception> exceptionFactory) + where TKey : notnull + { + if (parameter is null || parameter.ContainsKey(key)) + { + Throw.CustomException(exceptionFactory, parameter, key); + } + + return parameter; + } + /// /// Ensures that the string does not end with the specified value, or otherwise throws a . /// @@ -8457,6 +8637,27 @@ protected ExistingItemException(SerializationInfo info, StreamingContext context } } + /// + /// This exception indicates that a dictionary contains a key that must not be part of it. + /// + [Serializable] + internal class ExistingKeyException : CollectionException + { + /// + /// Creates a new instance of . + /// + /// The name of the parameter (optional). + /// The message of the exception (optional). + public ExistingKeyException(string? parameterName = null, string? message = null) : base(parameterName, message) + { + } + + /// + protected ExistingKeyException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } + /// /// This exception indicates that a collection has an invalid number of items. /// @@ -8604,6 +8805,27 @@ protected MissingItemException(SerializationInfo info, StreamingContext context) } } + /// + /// This exception indicates that a key is not present in a dictionary. + /// + [Serializable] + internal class MissingKeyException : CollectionException + { + /// + /// Creates a new instance of . + /// + /// The name of the parameter (optional). + /// The message of the exception (optional). + public MissingKeyException(string? parameterName = null, string? message = null) : base(parameterName, message) + { + } + + /// + protected MissingKeyException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } + /// /// This exception indicates that a has no value. /// @@ -9059,6 +9281,13 @@ public static void EnumValueNotDefined(T parameter, [CallerArgumentExpression [DoesNotReturn] public static void ExistingItem(IEnumerable parameter, TItem item, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) => throw new ExistingItemException(parameterName, message ?? new StringBuilder().AppendLine($"{parameterName ?? "The collection"} must not contain {item.ToStringOrNull()}, but it actually does.").AppendCollectionContent(parameter).ToString()); /// + /// Throws the default indicating that a dictionary contains the specified key + /// that should not be part of it, using the optional parameter name and message. + /// + [ContractAnnotation("=> halt")] + [DoesNotReturn] + public static void ExistingKey(IReadOnlyDictionary parameter, TKey key, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) => throw new ExistingKeyException(parameterName, message ?? $"{parameterName ?? "The dictionary"} must not contain key {key.ToStringOrNull()}, but it actually does."); + /// /// Throws the default indicating that an 's length is not within the /// given range, using the optional parameter name and message. /// @@ -9159,6 +9388,13 @@ public static void InvalidMinimumImmutableArrayLength(ImmutableArray param [DoesNotReturn] public static void MissingItem(IEnumerable parameter, TItem item, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) => throw new MissingItemException(parameterName, message ?? new StringBuilder().AppendLine($"{parameterName ?? "The collection"} must contain {item.ToStringOrNull()}, but it actually does not.").AppendCollectionContent(parameter).ToString()); /// + /// Throws the default indicating that a dictionary is not containing the + /// specified key, using the optional parameter name and message. + /// + [ContractAnnotation("=> halt")] + [DoesNotReturn] + public static void MissingKey(IReadOnlyDictionary parameter, TKey key, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) => throw new MissingKeyException(parameterName, message ?? new StringBuilder().AppendLine($"{parameterName ?? "The dictionary"} must contain key {key.ToStringOrNull()}, but it actually does not.").AppendCollectionContent(parameter.Keys, "Keys of the dictionary:").ToString()); + /// /// Throws the default indicating that a value must be approximately /// equal to another value within a specified tolerance, using the optional parameter name and message. /// diff --git a/ai-plans/0148-dictionary-key-guards.md b/ai-plans/0148-dictionary-key-guards.md new file mode 100644 index 0000000..c0dc14e --- /dev/null +++ b/ai-plans/0148-dictionary-key-guards.md @@ -0,0 +1,57 @@ +# Dictionary Key Guards + +## Rationale + +Line-of-Business code constantly guards lookup structures — settings dictionaries, translation tables, header maps — but `MustContain` and `MustNotContain` operate on collection items and substrings, so there is no assertion that targets dictionary keys. Versions before the v4 rewrite shipped `MustContainKey` and `MustNotContainKey` in the removed `DictionaryAssertions` class; reintroduce these two guards with the library's modern conventions. The broader legacy family (`MustContainValue`, `MustContainPair`, `MustBeKeyOf`, plural-keys overloads) is deliberately not resurrected: value and pair lookups are O(n) and rarely precondition-shaped, and they can be added later on demand. + +The additions must preserve the library's fluent return values, exception-factory overloads, nullable annotations, broad target-framework support, Native AOT compatibility, and the customizable single-file source distribution. Key lookups must use `ContainsKey` so they never enumerate and honor the dictionary's own key comparer. + +## Acceptance Criteria + +- [x] `MustContainKey` and `MustNotContainKey` are available for `IReadOnlyDictionary` receivers on .NET Standard 2.0, .NET Standard 2.1, and .NET 10 with identical semantics, and both type arguments are inferred at the call site without explicit specification. +- [x] Additional overloads for `Dictionary` preserve and return the concrete dictionary shape, while receivers of other dictionary types implementing `IReadOnlyDictionary` (such as `ConcurrentDictionary`, `SortedDictionary`, `ReadOnlyDictionary`, `ImmutableDictionary`, and `FrozenDictionary` on .NET 10) bind to the interface overloads without overload-resolution ambiguity. +- [x] The guards check key presence exclusively through `ContainsKey`, so they never enumerate the dictionary and respect its configured key comparer. +- [x] A null dictionary thrown into the default overloads produces the established `ArgumentNullException` null behavior; the exception-factory overloads pass the dictionary and the key to the factory. +- [x] A failed `MustContainKey` throws the new `MissingKeyException` and a failed `MustNotContainKey` throws the new `ExistingKeyException`; both derive from `CollectionException`, report the offending key, and follow the existing serialization conventions. +- [x] Every new guard returns the successfully validated input, captures the guarded expression for the default exception, accepts an optional custom message, and provides an exception-factory overload consistent with the existing API. +- [x] Automated tests cover present and absent keys, null dictionaries, comparer-sensitive lookups (for example an `OrdinalIgnoreCase` dictionary), default exceptions, custom messages, custom exception factories, caller argument expressions, returned values including the preserved `Dictionary` shape, and successful binding for several dictionary types. +- [x] The source-export whitelist catalog and committed settings contain the two new assertion families with their exceptions and throw helpers, and focused source-export tests cover the new entries. +- [x] The committed .NET Standard 2.0 single-file distribution is regenerated and validates with the new portable API surface. +- [x] The assertion overview documents the new guards, including the shapes they accept and the `IDictionary`-only limitation described below. +- [x] The complete solution restores and builds without warnings in Release configuration, and all automated tests pass on the pinned SDK. + +## Technical Details + +One `Check..cs` file per family. The `MustContain`-style shape preservation via a `TCollection` type parameter is impossible here: C# type inference does not consult generic constraints, so a `TDictionary : class, IReadOnlyDictionary` constraint leaves `TValue` non-inferable and would force explicit type arguments at every call site. The receivers are therefore interface-typed, with `TKey` and `TValue` inferred from the receiver conversion (exact signatures): + +```csharp +public static IReadOnlyDictionary MustContainKey( + [NotNull, ValidatedNotNull] this IReadOnlyDictionary? parameter, + TKey key, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null +); + +public static IReadOnlyDictionary MustContainKey( + this IReadOnlyDictionary? parameter, + TKey key, + Func?, TKey, Exception> exceptionFactory +); + +public static Dictionary MustContainKey( + [NotNull, ValidatedNotNull] this Dictionary? parameter, + TKey key, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null +) where TKey : notnull; +``` + +`MustNotContainKey` mirrors these shapes, and the `Dictionary` overloads also get factory variants. The concrete overloads exist because the interface overloads erase the shape in fluent chains (`_map = map.MustNotBeNull().MustContainKey("endpoint");` must keep compiling when `map` and `_map` are `Dictionary`); they coexist safely with the interface overloads because an identity conversion beats an interface conversion in overload resolution. The `notnull` constraint matches the BCL's `Dictionary` annotation and avoids nullability warnings in the implementation. + +**Why `IReadOnlyDictionary` and not `IDictionary` (or both).** A guard only reads, and every current BCL dictionary type implements `IReadOnlyDictionary`. Offering overloads for both interfaces would make every call on a concrete dictionary type other than `Dictionary` ambiguous (CS0121), because such types convert to both interfaces and neither conversion is better. Accepted limitation: a receiver statically typed as `IDictionary` cannot use the guards; document `dictionary.Keys.MustContain(key)` as the workaround (the key collections of the BCL dictionaries implement `ICollection.Contains` via `ContainsKey`, so this stays O(1)). + +The key is passed to `ContainsKey` unmodified; dictionaries that reject null keys surface their own `ArgumentNullException`, and the guards add no redundant key-null check. Follow `MustContain`'s null handling for the dictionary itself: `[NotNull, ValidatedNotNull]`, `[ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")]`, and the `MustNotBeNull` path in the default overloads. + +**Exceptions.** Add `MissingKeyException` and `ExistingKeyException` deriving from `CollectionException`, mirroring `MissingItemException`/`ExistingItemException` including the `#if !NET8_0_OR_GREATER` serialization constructor. Add `Throw.MissingKey` and `Throw.ExistingKey` helpers following the `Throw.MissingItem` message conventions — for example `"{parameterName ?? "The dictionary"} must contain key {key.ToStringOrNull()}, but it actually does not."` — appending the dictionary's keys (not its key-value pairs) via the existing collection-content helper for the missing-key case. + +Update `AssertionWhitelist`, `settings.json`, and the source-export whitelist tests for the two new families, ensure the new exceptions and throw helpers are reachable in the export, regenerate `Light.GuardClauses.SingleFile.cs` as the .NET Standard 2.0 output, and verify both portable and .NET 10 generated-source validation. Document the new guards in the collections section of `docs/assertion-overview.md`. No microbenchmarks are needed: the guards perform a single hash or tree lookup. diff --git a/docs/assertion-overview.md b/docs/assertion-overview.md index 3d570b6..84b0dd4 100644 --- a/docs/assertion-overview.md +++ b/docs/assertion-overview.md @@ -91,11 +91,14 @@ percentage.MustBeIn(Range.FromExclusive(0).ToInclusive(100)); | `MustHaveMinimumCount`, `MustHaveMaximumCount` | Enforce inclusive collection-count bounds | | `IsOneOf`, `MustBeOneOf`, `MustNotBeOneOf` | Test or enforce membership among supplied values | | `MustContain`, `MustNotContain` | Require or reject an item in collections and immutable arrays; string overloads operate on substrings | +| `MustContainKey`, `MustNotContainKey` | Require or reject a dictionary key via `ContainsKey`, never enumerating and honoring the dictionary's key comparer; accept `IReadOnlyDictionary` receivers, with dedicated overloads preserving the `Dictionary` shape | | `MustNotBeDefaultOrEmpty` | Requires an initialized, non-empty `ImmutableArray` | | `MustHaveLength`, `MustHaveLengthIn`, `MustHaveMinimumLength`, `MustHaveMaximumLength` | Validate string, span, or immutable-array length as provided by their overloads | Collection overloads preserve and return the original collection shape where possible. Check the XML documentation before using an `IEnumerable` guard in a hot path; annotations identify guards that do not enumerate. +The dictionary key guards bind to any dictionary type implementing `IReadOnlyDictionary` (such as `ConcurrentDictionary`, `SortedDictionary`, `ReadOnlyDictionary`, `ImmutableDictionary`, and `FrozenDictionary` on .NET 10). A receiver statically typed as `IDictionary` cannot use them; call `dictionary.Keys.MustContain(key)` as a workaround — the key collections of the BCL dictionaries implement `ICollection.Contains` via `ContainsKey`, so this stays O(1). + ## Text, character, span, and memory assertions | Assertion family | Behavior | diff --git a/src/Light.GuardClauses/Check.MustContainKey.cs b/src/Light.GuardClauses/Check.MustContainKey.cs new file mode 100644 index 0000000..7f451d2 --- /dev/null +++ b/src/Light.GuardClauses/Check.MustContainKey.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using JetBrains.Annotations; +using Light.GuardClauses.ExceptionFactory; +using Light.GuardClauses.Exceptions; +using NotNullAttribute = System.Diagnostics.CodeAnalysis.NotNullAttribute; + +namespace Light.GuardClauses; + +public static partial class Check +{ + /// + /// Ensures that the dictionary contains the specified key, or otherwise throws a . + /// The check is performed via , so the dictionary is + /// never enumerated and its key comparer is respected. + /// + /// The dictionary to be checked. + /// The key that must be present in the dictionary. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// Thrown when does not contain . + /// Thrown when is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] + public static IReadOnlyDictionary MustContainKey( + [NotNull] [ValidatedNotNull] this IReadOnlyDictionary? parameter, + TKey key, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + if (!parameter.MustNotBeNull(parameterName, message).ContainsKey(key)) + { + Throw.MissingKey(parameter, key, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the dictionary contains the specified key, or otherwise throws your custom exception. + /// The check is performed via , so the dictionary is + /// never enumerated and its key comparer is respected. + /// + /// The dictionary to be checked. + /// The key that must be present in the dictionary. + /// The delegate that creates your custom exception. and are passed to this delegate. + /// Your custom exception thrown when does not contain , or when is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] + public static IReadOnlyDictionary MustContainKey( + [NotNull] [ValidatedNotNull] this IReadOnlyDictionary? parameter, + TKey key, + Func?, TKey, Exception> exceptionFactory + ) + { + if (parameter is null || !parameter.ContainsKey(key)) + { + Throw.CustomException(exceptionFactory, parameter, key); + } + + return parameter; + } + + /// + /// Ensures that the dictionary contains the specified key, or otherwise throws a . + /// The check is performed via , so the dictionary is + /// never enumerated and its key comparer is respected. + /// + /// The dictionary to be checked. + /// The key that must be present in the dictionary. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// Thrown when does not contain . + /// Thrown when is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] + public static Dictionary MustContainKey( + [NotNull] [ValidatedNotNull] this Dictionary? parameter, + TKey key, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) where TKey : notnull + { + if (!parameter.MustNotBeNull(parameterName, message).ContainsKey(key)) + { + Throw.MissingKey(parameter, key, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the dictionary contains the specified key, or otherwise throws your custom exception. + /// The check is performed via , so the dictionary is + /// never enumerated and its key comparer is respected. + /// + /// The dictionary to be checked. + /// The key that must be present in the dictionary. + /// The delegate that creates your custom exception. and are passed to this delegate. + /// Your custom exception thrown when does not contain , or when is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] + public static Dictionary MustContainKey( + [NotNull] [ValidatedNotNull] this Dictionary? parameter, + TKey key, + Func?, TKey, Exception> exceptionFactory + ) where TKey : notnull + { + if (parameter is null || !parameter.ContainsKey(key)) + { + Throw.CustomException(exceptionFactory, parameter, key); + } + + return parameter; + } +} diff --git a/src/Light.GuardClauses/Check.MustNotContainKey.cs b/src/Light.GuardClauses/Check.MustNotContainKey.cs new file mode 100644 index 0000000..3c45274 --- /dev/null +++ b/src/Light.GuardClauses/Check.MustNotContainKey.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using JetBrains.Annotations; +using Light.GuardClauses.ExceptionFactory; +using Light.GuardClauses.Exceptions; +using NotNullAttribute = System.Diagnostics.CodeAnalysis.NotNullAttribute; + +namespace Light.GuardClauses; + +public static partial class Check +{ + /// + /// Ensures that the dictionary does not contain the specified key, or otherwise throws an . + /// The check is performed via , so the dictionary is + /// never enumerated and its key comparer is respected. + /// + /// The dictionary to be checked. + /// The key that must not be present in the dictionary. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// Thrown when contains . + /// Thrown when is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] + public static IReadOnlyDictionary MustNotContainKey( + [NotNull] [ValidatedNotNull] this IReadOnlyDictionary? parameter, + TKey key, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + if (parameter.MustNotBeNull(parameterName, message).ContainsKey(key)) + { + Throw.ExistingKey(parameter, key, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the dictionary does not contain the specified key, or otherwise throws your custom exception. + /// The check is performed via , so the dictionary is + /// never enumerated and its key comparer is respected. + /// + /// The dictionary to be checked. + /// The key that must not be present in the dictionary. + /// The delegate that creates your custom exception. and are passed to this delegate. + /// Your custom exception thrown when contains , or when is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] + public static IReadOnlyDictionary MustNotContainKey( + [NotNull] [ValidatedNotNull] this IReadOnlyDictionary? parameter, + TKey key, + Func?, TKey, Exception> exceptionFactory + ) + { + if (parameter is null || parameter.ContainsKey(key)) + { + Throw.CustomException(exceptionFactory, parameter, key); + } + + return parameter; + } + + /// + /// Ensures that the dictionary does not contain the specified key, or otherwise throws an . + /// The check is performed via , so the dictionary is + /// never enumerated and its key comparer is respected. + /// + /// The dictionary to be checked. + /// The key that must not be present in the dictionary. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// Thrown when contains . + /// Thrown when is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] + public static Dictionary MustNotContainKey( + [NotNull] [ValidatedNotNull] this Dictionary? parameter, + TKey key, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) where TKey : notnull + { + if (parameter.MustNotBeNull(parameterName, message).ContainsKey(key)) + { + Throw.ExistingKey(parameter, key, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the dictionary does not contain the specified key, or otherwise throws your custom exception. + /// The check is performed via , so the dictionary is + /// never enumerated and its key comparer is respected. + /// + /// The dictionary to be checked. + /// The key that must not be present in the dictionary. + /// The delegate that creates your custom exception. and are passed to this delegate. + /// Your custom exception thrown when contains , or when is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] + public static Dictionary MustNotContainKey( + [NotNull] [ValidatedNotNull] this Dictionary? parameter, + TKey key, + Func?, TKey, Exception> exceptionFactory + ) where TKey : notnull + { + if (parameter is null || parameter.ContainsKey(key)) + { + Throw.CustomException(exceptionFactory, parameter, key); + } + + return parameter; + } +} diff --git a/src/Light.GuardClauses/ExceptionFactory/Throw.ExistingKey.cs b/src/Light.GuardClauses/ExceptionFactory/Throw.ExistingKey.cs new file mode 100644 index 0000000..7c9a2e2 --- /dev/null +++ b/src/Light.GuardClauses/ExceptionFactory/Throw.ExistingKey.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using JetBrains.Annotations; +using Light.GuardClauses.Exceptions; +using Light.GuardClauses.FrameworkExtensions; + +namespace Light.GuardClauses.ExceptionFactory; + +public static partial class Throw +{ + /// + /// Throws the default indicating that a dictionary contains the specified key + /// that should not be part of it, using the optional parameter name and message. + /// + [ContractAnnotation("=> halt")] + [DoesNotReturn] + public static void ExistingKey( + IReadOnlyDictionary parameter, + TKey key, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) => + throw new ExistingKeyException( + parameterName, + message ?? + $"{parameterName ?? "The dictionary"} must not contain key {key.ToStringOrNull()}, but it actually does." + ); +} diff --git a/src/Light.GuardClauses/ExceptionFactory/Throw.MissingKey.cs b/src/Light.GuardClauses/ExceptionFactory/Throw.MissingKey.cs new file mode 100644 index 0000000..996eaa5 --- /dev/null +++ b/src/Light.GuardClauses/ExceptionFactory/Throw.MissingKey.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Text; +using JetBrains.Annotations; +using Light.GuardClauses.Exceptions; +using Light.GuardClauses.FrameworkExtensions; + +namespace Light.GuardClauses.ExceptionFactory; + +public static partial class Throw +{ + /// + /// Throws the default indicating that a dictionary is not containing the + /// specified key, using the optional parameter name and message. + /// + [ContractAnnotation("=> halt")] + [DoesNotReturn] + public static void MissingKey( + IReadOnlyDictionary parameter, + TKey key, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) => + throw new MissingKeyException( + parameterName, + message ?? + new StringBuilder() + .AppendLine( + $"{parameterName ?? "The dictionary"} must contain key {key.ToStringOrNull()}, but it actually does not." + ) + .AppendCollectionContent(parameter.Keys, "Keys of the dictionary:") + .ToString() + ); +} diff --git a/src/Light.GuardClauses/Exceptions/ExistingKeyException.cs b/src/Light.GuardClauses/Exceptions/ExistingKeyException.cs new file mode 100644 index 0000000..75486b1 --- /dev/null +++ b/src/Light.GuardClauses/Exceptions/ExistingKeyException.cs @@ -0,0 +1,23 @@ +using System; +using System.Runtime.Serialization; + +namespace Light.GuardClauses.Exceptions; + +/// +/// This exception indicates that a dictionary contains a key that must not be part of it. +/// +[Serializable] +public class ExistingKeyException : CollectionException +{ + /// + /// Creates a new instance of . + /// + /// The name of the parameter (optional). + /// The message of the exception (optional). + public ExistingKeyException(string? parameterName = null, string? message = null) : base(parameterName, message) { } + +#if !NET8_0_OR_GREATER + /// + protected ExistingKeyException(SerializationInfo info, StreamingContext context) : base(info, context) { } +#endif +} diff --git a/src/Light.GuardClauses/Exceptions/MissingKeyException.cs b/src/Light.GuardClauses/Exceptions/MissingKeyException.cs new file mode 100644 index 0000000..be03b89 --- /dev/null +++ b/src/Light.GuardClauses/Exceptions/MissingKeyException.cs @@ -0,0 +1,23 @@ +using System; +using System.Runtime.Serialization; + +namespace Light.GuardClauses.Exceptions; + +/// +/// This exception indicates that a key is not present in a dictionary. +/// +[Serializable] +public class MissingKeyException : CollectionException +{ + /// + /// Creates a new instance of . + /// + /// The name of the parameter (optional). + /// The message of the exception (optional). + public MissingKeyException(string? parameterName = null, string? message = null) : base(parameterName, message) { } + +#if !NET8_0_OR_GREATER + /// + protected MissingKeyException(SerializationInfo info, StreamingContext context) : base(info, context) { } +#endif +} diff --git a/tests/Light.GuardClauses.SourceCodeTransformation.Tests/SourceFileMergerWhitelistTests.cs b/tests/Light.GuardClauses.SourceCodeTransformation.Tests/SourceFileMergerWhitelistTests.cs index ace322e..0c671d9 100644 --- a/tests/Light.GuardClauses.SourceCodeTransformation.Tests/SourceFileMergerWhitelistTests.cs +++ b/tests/Light.GuardClauses.SourceCodeTransformation.Tests/SourceFileMergerWhitelistTests.cs @@ -232,6 +232,60 @@ public static void SignGuardWhitelistsUseTargetSpecificSurface() modernCode.Should().Contain("INumber"); } + [Fact] + public static void MustContainKeyWhitelistExportsGuardWithExceptionAndThrowHelper() + { + using var temporaryDirectory = new TemporaryDirectory(); + var targetFile = Path.Combine(temporaryDirectory.DirectoryPath, "MustContainKey.cs"); + + SourceFileMerger.CreateSingleSourceFile( + CreateOptions( + targetFile, + CreateWhitelist( + includedAssertions: [new ("MustContainKey", true)] + ) + ) + ); + var sourceCode = File.ReadAllText(targetFile); + + sourceCode.Should().Contain("MustContainKey("); + sourceCode.Should().Contain("this IReadOnlyDictionary? parameter"); + sourceCode.Should().Contain("this Dictionary? parameter"); + sourceCode.Should().Contain("class MissingKeyException : CollectionException"); + sourceCode.Should().Contain("public static void MissingKey("); + sourceCode.Should() + .Contain("Func?, TKey, Exception> exceptionFactory"); + sourceCode.Should().Contain("Func?, TKey, Exception> exceptionFactory"); + sourceCode.Should().NotContain("MustNotContainKey"); + sourceCode.Should().NotContain("class ExistingKeyException"); + } + + [Fact] + public static void MustNotContainKeyWhitelistExportsGuardAndTrimsExceptionFactoryOverloads() + { + using var temporaryDirectory = new TemporaryDirectory(); + var targetFile = Path.Combine(temporaryDirectory.DirectoryPath, "MustNotContainKey.cs"); + + SourceFileMerger.CreateSingleSourceFile( + CreateOptions( + targetFile, + CreateWhitelist( + includedAssertions: [new ("MustNotContainKey", false)] + ) + ) + ); + var sourceCode = File.ReadAllText(targetFile); + + sourceCode.Should().Contain("MustNotContainKey("); + sourceCode.Should().Contain("this IReadOnlyDictionary? parameter"); + sourceCode.Should().Contain("this Dictionary? parameter"); + sourceCode.Should().Contain("class ExistingKeyException : CollectionException"); + sourceCode.Should().Contain("public static void ExistingKey("); + sourceCode.Should().NotContain("TKey, Exception> exceptionFactory"); + sourceCode.Should().NotContain("MustContainKey("); + sourceCode.Should().NotContain("class MissingKeyException"); + } + private static SourceFileMergeOptions CreateOptions( string targetFile, AssertionWhitelist assertionWhitelist = null, diff --git a/tests/Light.GuardClauses.Tests/CollectionAssertions/MustContainKeyTests.cs b/tests/Light.GuardClauses.Tests/CollectionAssertions/MustContainKeyTests.cs new file mode 100644 index 0000000..dc5fd19 --- /dev/null +++ b/tests/Light.GuardClauses.Tests/CollectionAssertions/MustContainKeyTests.cs @@ -0,0 +1,208 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Collections.ObjectModel; +using FluentAssertions; +using Light.GuardClauses.Exceptions; +using Xunit; +#if NET8_0_OR_GREATER +using System.Collections.Frozen; +#endif + +namespace Light.GuardClauses.Tests.CollectionAssertions; + +public static class MustContainKeyTests +{ + [Fact] + public static void KeyNotPresent() + { + var dictionary = new Dictionary { ["Foo"] = 1, ["Bar"] = 2 }; + + Action act = () => dictionary.MustContainKey("Baz", nameof(dictionary)); + + var assertion = act.Should().Throw().Which; + assertion.Message.Should() + .Contain($"{nameof(dictionary)} must contain key \"Baz\", but it actually does not."); + assertion.Message.Should().Contain("Keys of the dictionary:"); + assertion.Message.Should().Contain("\"Foo\""); + } + + [Fact] + public static void KeyPresent() + { + var dictionary = new Dictionary { [42] = "Foo", [86] = "Bar" }; + + dictionary.MustContainKey(42).Should().BeSameAs(dictionary); + } + + [Fact] + public static void InterfaceKeyNotPresent() + { + IReadOnlyDictionary dictionary = new Dictionary { ["Foo"] = 1 }; + + Action act = () => dictionary.MustContainKey("Bar", nameof(dictionary)); + + act.Should().Throw() + .And.Message.Should() + .Contain($"{nameof(dictionary)} must contain key \"Bar\", but it actually does not."); + } + + [Fact] + public static void InterfaceKeyPresent() + { + IReadOnlyDictionary dictionary = new Dictionary { ["Foo"] = 1 }; + + dictionary.MustContainKey("Foo").Should().BeSameAs(dictionary); + } + + [Fact] + public static void DictionaryNull() + { + Action act = () => ((Dictionary) null).MustContainKey("Foo"); + + act.Should().Throw(); + } + + [Fact] + public static void InterfaceDictionaryNull() + { + Action act = () => ((IReadOnlyDictionary) null).MustContainKey("Foo"); + + act.Should().Throw(); + } + + [Fact] + public static void KeyComparerIsRespected() + { + var dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["FOO"] = 1 }; + + dictionary.MustContainKey("foo").Should().BeSameAs(dictionary); + } + + [Fact] + public static void OrdinalKeyComparerDoesNotMatchDifferentCasing() + { + var dictionary = new Dictionary(StringComparer.Ordinal) { ["FOO"] = 1 }; + + Action act = () => dictionary.MustContainKey("foo"); + + act.Should().Throw(); + } + + [Fact] + public static void CustomException() + { + var dictionary = new Dictionary { ["Foo"] = 1 }; + + Test.CustomException( + dictionary, + "Bar", + (d, key, exceptionFactory) => d.MustContainKey(key, exceptionFactory) + ); + } + + [Fact] + public static void CustomExceptionDictionaryNull() => + Test.CustomException( + (Dictionary) null, + "Foo", + (d, key, exceptionFactory) => d.MustContainKey(key, exceptionFactory) + ); + + [Fact] + public static void InterfaceCustomException() + { + IReadOnlyDictionary dictionary = new Dictionary { ["Foo"] = 1 }; + + Test.CustomException( + dictionary, + "Bar", + (d, key, exceptionFactory) => d.MustContainKey(key, exceptionFactory) + ); + } + + [Fact] + public static void CustomExceptionNotThrown() + { + var dictionary = new Dictionary { ["Foo"] = 1 }; + + dictionary.MustContainKey("Foo", (_, _) => new Exception()).Should().BeSameAs(dictionary); + } + + [Fact] + public static void CustomMessage() => + Test.CustomMessage( + message => new Dictionary().MustContainKey("Foo", message: message) + ); + + [Fact] + public static void CustomMessageDictionaryNull() => + Test.CustomMessage( + message => ((Dictionary) null).MustContainKey("Foo", message: message) + ); + + [Fact] + public static void CallerArgumentExpression() + { + var settings = new Dictionary { ["Foo"] = "Bar" }; + + var act = () => settings.MustContainKey("Baz"); + + act.Should().Throw() + .WithParameterName(nameof(settings)); + } + + [Fact] + public static void DictionaryShapeIsPreservedInFluentChains() + { + var map = new Dictionary { ["endpoint"] = "https://example.com" }; + + Dictionary result = map.MustNotBeNull().MustContainKey("endpoint"); + + result.Should().BeSameAs(map); + } + + [Fact] + public static void ConcurrentDictionaryBindsToInterfaceOverload() + { + var dictionary = new ConcurrentDictionary(); + dictionary.TryAdd("Foo", 1).Should().BeTrue(); + + dictionary.MustContainKey("Foo").Should().BeSameAs(dictionary); + } + + [Fact] + public static void SortedDictionaryBindsToInterfaceOverload() + { + var dictionary = new SortedDictionary { ["Foo"] = 1 }; + + dictionary.MustContainKey("Foo").Should().BeSameAs(dictionary); + } + + [Fact] + public static void ReadOnlyDictionaryBindsToInterfaceOverload() + { + var dictionary = new ReadOnlyDictionary(new Dictionary { ["Foo"] = 1 }); + + dictionary.MustContainKey("Foo").Should().BeSameAs(dictionary); + } + + [Fact] + public static void ImmutableDictionaryBindsToInterfaceOverload() + { + var dictionary = ImmutableDictionary.Empty.Add("Foo", 1); + + dictionary.MustContainKey("Foo").Should().BeSameAs(dictionary); + } + +#if NET8_0_OR_GREATER + [Fact] + public static void FrozenDictionaryBindsToInterfaceOverload() + { + var dictionary = new Dictionary { ["Foo"] = 1 }.ToFrozenDictionary(); + + dictionary.MustContainKey("Foo").Should().BeSameAs(dictionary); + } +#endif +} diff --git a/tests/Light.GuardClauses.Tests/CollectionAssertions/MustNotContainKeyTests.cs b/tests/Light.GuardClauses.Tests/CollectionAssertions/MustNotContainKeyTests.cs new file mode 100644 index 0000000..3972f77 --- /dev/null +++ b/tests/Light.GuardClauses.Tests/CollectionAssertions/MustNotContainKeyTests.cs @@ -0,0 +1,206 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Collections.ObjectModel; +using FluentAssertions; +using Light.GuardClauses.Exceptions; +using Xunit; +#if NET8_0_OR_GREATER +using System.Collections.Frozen; +#endif + +namespace Light.GuardClauses.Tests.CollectionAssertions; + +public static class MustNotContainKeyTests +{ + [Fact] + public static void KeyPresent() + { + var dictionary = new Dictionary { ["Foo"] = 1, ["Bar"] = 2 }; + + Action act = () => dictionary.MustNotContainKey("Foo", nameof(dictionary)); + + act.Should().Throw() + .And.Message.Should() + .Contain($"{nameof(dictionary)} must not contain key \"Foo\", but it actually does."); + } + + [Fact] + public static void KeyNotPresent() + { + var dictionary = new Dictionary { [42] = "Foo" }; + + dictionary.MustNotContainKey(86).Should().BeSameAs(dictionary); + } + + [Fact] + public static void InterfaceKeyPresent() + { + IReadOnlyDictionary dictionary = new Dictionary { ["Foo"] = 1 }; + + Action act = () => dictionary.MustNotContainKey("Foo", nameof(dictionary)); + + act.Should().Throw() + .And.Message.Should() + .Contain($"{nameof(dictionary)} must not contain key \"Foo\", but it actually does."); + } + + [Fact] + public static void InterfaceKeyNotPresent() + { + IReadOnlyDictionary dictionary = new Dictionary { ["Foo"] = 1 }; + + dictionary.MustNotContainKey("Bar").Should().BeSameAs(dictionary); + } + + [Fact] + public static void DictionaryNull() + { + Action act = () => ((Dictionary) null).MustNotContainKey("Foo"); + + act.Should().Throw(); + } + + [Fact] + public static void InterfaceDictionaryNull() + { + Action act = () => ((IReadOnlyDictionary) null).MustNotContainKey("Foo"); + + act.Should().Throw(); + } + + [Fact] + public static void KeyComparerIsRespected() + { + var dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["FOO"] = 1 }; + + Action act = () => dictionary.MustNotContainKey("foo"); + + act.Should().Throw(); + } + + [Fact] + public static void OrdinalKeyComparerDoesNotMatchDifferentCasing() + { + var dictionary = new Dictionary(StringComparer.Ordinal) { ["FOO"] = 1 }; + + dictionary.MustNotContainKey("foo").Should().BeSameAs(dictionary); + } + + [Fact] + public static void CustomException() + { + var dictionary = new Dictionary { ["Foo"] = 1 }; + + Test.CustomException( + dictionary, + "Foo", + (d, key, exceptionFactory) => d.MustNotContainKey(key, exceptionFactory) + ); + } + + [Fact] + public static void CustomExceptionDictionaryNull() => + Test.CustomException( + (Dictionary) null, + "Foo", + (d, key, exceptionFactory) => d.MustNotContainKey(key, exceptionFactory) + ); + + [Fact] + public static void InterfaceCustomException() + { + IReadOnlyDictionary dictionary = new Dictionary { ["Foo"] = 1 }; + + Test.CustomException( + dictionary, + "Foo", + (d, key, exceptionFactory) => d.MustNotContainKey(key, exceptionFactory) + ); + } + + [Fact] + public static void CustomExceptionNotThrown() + { + var dictionary = new Dictionary { ["Foo"] = 1 }; + + dictionary.MustNotContainKey("Bar", (_, _) => new Exception()).Should().BeSameAs(dictionary); + } + + [Fact] + public static void CustomMessage() => + Test.CustomMessage( + message => new Dictionary { ["Foo"] = 1 }.MustNotContainKey("Foo", message: message) + ); + + [Fact] + public static void CustomMessageDictionaryNull() => + Test.CustomMessage( + message => ((Dictionary) null).MustNotContainKey("Foo", message: message) + ); + + [Fact] + public static void CallerArgumentExpression() + { + var settings = new Dictionary { ["Foo"] = "Bar" }; + + var act = () => settings.MustNotContainKey("Foo"); + + act.Should().Throw() + .WithParameterName(nameof(settings)); + } + + [Fact] + public static void DictionaryShapeIsPreservedInFluentChains() + { + var map = new Dictionary { ["endpoint"] = "https://example.com" }; + + Dictionary result = map.MustNotBeNull().MustNotContainKey("proxy"); + + result.Should().BeSameAs(map); + } + + [Fact] + public static void ConcurrentDictionaryBindsToInterfaceOverload() + { + var dictionary = new ConcurrentDictionary(); + dictionary.TryAdd("Foo", 1).Should().BeTrue(); + + dictionary.MustNotContainKey("Bar").Should().BeSameAs(dictionary); + } + + [Fact] + public static void SortedDictionaryBindsToInterfaceOverload() + { + var dictionary = new SortedDictionary { ["Foo"] = 1 }; + + dictionary.MustNotContainKey("Bar").Should().BeSameAs(dictionary); + } + + [Fact] + public static void ReadOnlyDictionaryBindsToInterfaceOverload() + { + var dictionary = new ReadOnlyDictionary(new Dictionary { ["Foo"] = 1 }); + + dictionary.MustNotContainKey("Bar").Should().BeSameAs(dictionary); + } + + [Fact] + public static void ImmutableDictionaryBindsToInterfaceOverload() + { + var dictionary = ImmutableDictionary.Empty.Add("Foo", 1); + + dictionary.MustNotContainKey("Bar").Should().BeSameAs(dictionary); + } + +#if NET8_0_OR_GREATER + [Fact] + public static void FrozenDictionaryBindsToInterfaceOverload() + { + var dictionary = new Dictionary { ["Foo"] = 1 }.ToFrozenDictionary(); + + dictionary.MustNotContainKey("Bar").Should().BeSameAs(dictionary); + } +#endif +} diff --git a/tools/source-export/Light.GuardClauses.SourceCodeTransformation/AssertionWhitelist.cs b/tools/source-export/Light.GuardClauses.SourceCodeTransformation/AssertionWhitelist.cs index ebb0862..d0221e5 100644 --- a/tools/source-export/Light.GuardClauses.SourceCodeTransformation/AssertionWhitelist.cs +++ b/tools/source-export/Light.GuardClauses.SourceCodeTransformation/AssertionWhitelist.cs @@ -159,6 +159,8 @@ public sealed class AssertionWhitelist public AssertionEntry MustContain { get; init; } = new(); + public AssertionEntry MustContainKey { get; init; } = new(); + public AssertionEntry MustEndWith { get; init; } = new(); public AssertionEntry MustHaveCount { get; init; } = new(); @@ -229,6 +231,8 @@ public sealed class AssertionWhitelist public AssertionEntry MustNotContain { get; init; } = new(); + public AssertionEntry MustNotContainKey { get; init; } = new(); + public AssertionEntry MustNotEndWith { get; init; } = new(); public AssertionEntry MustNotStartWith { get; init; } = new(); diff --git a/tools/source-export/Light.GuardClauses.SourceCodeTransformation/settings.json b/tools/source-export/Light.GuardClauses.SourceCodeTransformation/settings.json index c19f91e..af58efd 100644 --- a/tools/source-export/Light.GuardClauses.SourceCodeTransformation/settings.json +++ b/tools/source-export/Light.GuardClauses.SourceCodeTransformation/settings.json @@ -93,6 +93,7 @@ "MustBeUuidVersion7": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustBeValidEnumValue": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustContain": { "Include": true, "IncludeExceptionFactoryOverload": true }, + "MustContainKey": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustEndWith": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustHaveCount": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustHaveCountIn": { "Include": true, "IncludeExceptionFactoryOverload": true }, @@ -128,6 +129,7 @@ "MustNotBeSubstringOf": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustNotBeZero": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustNotContain": { "Include": true, "IncludeExceptionFactoryOverload": true }, + "MustNotContainKey": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustNotEndWith": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustNotStartWith": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustStartWith": { "Include": true, "IncludeExceptionFactoryOverload": true }