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 Docs/pages/02-filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ In addition to [access modifiers](#access-modifiers),
| abstract / sealed *(properties only)* | `.WhichAreAbstract()` / `.WhichAreSealed()` | `.IsAbstract()` / `.IsSealed()` | `.AreAbstract()` / `.AreSealed()` |
| virtual *(properties only)* | `.WhichAreVirtual()` | `.IsVirtual()` | `.AreVirtual()` |
| override *(properties only)* | `.WhichOverride()` | `.Overrides()` | `.Override()` |
| required *(properties only)* | `.WhichAreRequired()` | `.IsRequired()` | `.AreRequired()` |
| required *(properties & fields)* | `.WhichAreRequired()` | `.IsRequired()` | `.AreRequired()` |
| readable *(properties only)* | `.WhichAreReadable()` | `.IsReadable()` | `.AreReadable()` |
| writable *(properties only)* | `.WhichAreWritable()` | `.IsWritable()` | `.AreWritable()` |
| read-only *(properties only)* | `.WhichAreReadOnly()` | `.IsReadOnly()` | `.AreReadOnly()` |
Expand Down
11 changes: 7 additions & 4 deletions Docs/pages/comparison/01-fluentassertions-comparison.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ Legend: ✅ dedicated assertion · ⚠️ only via a general mechanism (selector
| **Type**: member presence by signature | ✅ | ⚠️ |
| **Type**: operator presence (by kind) | ⚠️ | ✅ |
| **Type**: conversion operators by signature | ✅ | ✅ |
| **Type**: immutability / member nullability | ❌ | ✅ |
| **Type**: dependencies (on / only on / outside an allowed set) | ❌ | ✅ |
| **Method**: virtual / async / return type | ✅ | ✅ |
| **Method**: static / abstract / sealed / generic / extension / operator | ❌ | ✅ |
| **Method**: override | ❌ | ✅ |
Expand All @@ -50,9 +52,10 @@ Legend: ✅ dedicated assertion · ⚠️ only via a general mechanism (selector
| **Property**: readable / writable | ✅ | ✅ |
| **Property**: virtual | ✅ | ✅ |
| **Property**: static / abstract / required / indexer / init-setter / override | ❌ | ✅ |
| **Property**: nullable | ❌ | ✅ |
| **Property**: return / declared type | ✅ | ✅ |
| **Constructor**: dedicated assertions / filtering | ⚠️ | ✅ |
| **Field**: assertions | ❌ | ✅ |
| **Field**: assertions (static / read-only / constant / required / nullable) | ❌ | ✅ |
| **Event**: assertions | ❌ | ✅ |

The sections below detail each target with side-by-side examples.
Expand Down Expand Up @@ -91,7 +94,7 @@ await Expect.That(In.AllLoadedAssemblies()).HaveName("System").AsPrefix();

- **FluentAssertions** (`TypeAssertions`): `Be<T>`/`NotBe<T>`, `BeAssignableTo<T>`, `Implement<T>`/`NotImplement<T>`, `BeDerivedFrom<T>`, `BeAbstract`/`BeSealed`/`BeStatic`, `HaveAccessModifier(CSharpAccessModifier)`, `BeDecoratedWith<T>` (with attribute predicate and `OrInherit` variants), member presence (`HaveProperty`/`HaveMethod`/`HaveConstructor`/`HaveIndexer`/`HaveExplicit*`), and conversion operators. Type *kind* is available only as a selector filter (`ThatAreClasses()`), and namespace
only on the selector (`BeInNamespace`); a single type's name/namespace use the string API.
- **aweXpect.Reflection**: type-kind assertions (`IsAClass`, `IsAnInterface`, `IsAnEnum`, `IsAStruct`, `IsARecord`, `IsARecordStruct`, `IsARefStruct`, `IsADelegate`, `IsAnAttribute`, `IsAnException`), `IsAbstract`/`IsSealed`/`IsStatic`/`IsReadOnly`/`IsNested`/`IsGeneric`/`IsInstantiable`, access modifiers (`IsPublic`, …), `InheritsFrom<T>().Directly()`, `HasName`, `HasNamespace`/`IsWithinNamespace`, `Has<TAttribute>`, quantified member containment (`ContainsMethods()`, …), operator presence by
- **aweXpect.Reflection**: type-kind assertions (`IsAClass`, `IsAnInterface`, `IsAnEnum`, `IsAStruct`, `IsARecord`, `IsARecordStruct`, `IsARefStruct`, `IsADelegate`, `IsAnAttribute`, `IsAnException`), `IsAbstract`/`IsSealed`/`IsStatic`/`IsReadOnly`/`IsNested`/`IsGeneric`/`IsInstantiable`/`IsImmutable`, access modifiers (`IsPublic`, …), `InheritsFrom<T>().Directly()`, `HasName`, `HasNamespace`/`IsWithinNamespace`, `Has<TAttribute>`, quantified member containment (`ContainsMethods()`, …), member nullability (`OnlyHasNullableMembers`/`OnlyHasNonNullableMembers`), type-level dependencies (`DependsOn`/`DoesNotDependOn`/`DependsOnlyOn`/`HasDependenciesOutside`, against namespaces or type selections), operator presence by
kind (`HasOperator(Operator)`, `HasOperator<TOperand>(Operator)`) and conversion operators by signature (`HasImplicitConversionOperator<TSource, TTarget>`, `HasExplicitConversionOperator<TSource, TTarget>`).

<Tabs groupId="assertion-library">
Expand Down Expand Up @@ -169,7 +172,7 @@ await Expect.That(In.AssemblyContaining<MyClass>()
## Property

- **FluentAssertions** (`PropertyInfoAssertions`): `BeVirtual`/`NotBeVirtual`, `BeReadable`/`BeWritable` (each with an optional `CSharpAccessModifier`), `NotBeReadable`/`NotBeWritable`, `Return<T>`/`NotReturn<T>`, `BeDecoratedWith<T>`. No assertions for `static`/`abstract`/`sealed`/`required`/indexer/init-setter/override, and no name assertion.
- **aweXpect.Reflection**: `IsReadable`/`IsWritable`/`IsReadOnly`/`IsWriteOnly`/`IsReadWrite`, `HasAGetter`/`HasASetter`/`HasAnInitSetter`, `IsStatic`/`IsAbstract`/`IsSealed`/`IsVirtual`, `IsRequired`, `IsAnIndexer`, `Overrides<T>`, `IsOfType<T>`/`IsOfExactType<T>`, `HasName`, `Has<TAttribute>`.
- **aweXpect.Reflection**: `IsReadable`/`IsWritable`/`IsReadOnly`/`IsWriteOnly`/`IsReadWrite`, `HasAGetter`/`HasASetter`/`HasAnInitSetter`, `IsStatic`/`IsAbstract`/`IsSealed`/`IsVirtual`, `IsRequired`, `IsNullable`, `IsAnIndexer`, `IsAnExtensionProperty`, `Overrides<T>`, `IsOfType<T>`/`IsOfExactType<T>`, `HasName`, `Has<TAttribute>`.

<Tabs groupId="assertion-library">
<TabItem value="fluentassertions" label="FluentAssertions" default>
Expand Down Expand Up @@ -228,7 +231,7 @@ await Expect.That(typeof(MyClass)).HasADefaultConstructor();
## Field

- **FluentAssertions**: no `FieldInfo` assertions.
- **aweXpect.Reflection**: singular (`ThatField`) and plural (`ThatFields`) assertions and an `In.*…Fields()` filter: `IsStatic`, `IsReadOnly`, `IsConstant`, `IsOfType<T>`/`IsOfExactType<T>`, access modifiers (`IsPublic`, …), `HasName`, `Has<TAttribute>`.
- **aweXpect.Reflection**: singular (`ThatField`) and plural (`ThatFields`) assertions and an `In.*…Fields()` filter: `IsStatic`, `IsReadOnly`, `IsConstant`, `IsRequired`, `IsNullable`, `IsOfType<T>`/`IsOfExactType<T>`, access modifiers (`IsPublic`, …), `HasName`, `Has<TAttribute>`.

<Tabs groupId="assertion-library">
<TabItem value="fluentassertions" label="FluentAssertions" default>
Expand Down
4 changes: 2 additions & 2 deletions Docs/pages/comparison/02-architecture-comparison.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ Legend: ✅ built-in · ⚠️ partial or via a general mechanism · ❌ not ava
| Type kinds | ⚠️ classes, interfaces | ✅ classes, interfaces, enums, structs, records | ✅ plus delegates, exceptions, attributes, ref structs, record structs |
| Access modifiers | ⚠️ public, nested | ✅ | ✅ |
| Attribute rules | ✅ presence | ✅ incl. argument values | ✅ incl. typed predicate on the attribute instance |
| Member-level rules (methods, properties, fields) | ❌ | ✅ | ✅ plus events, constructors, parameter modifiers, async, extension methods, operators |
| Immutability / nullability rules | ✅ | ⚠️ immutability | |
| Member-level rules (methods, properties, fields) | ❌ | ✅ | ✅ plus events, constructors, parameter modifiers, async, extension methods, operators, required / nullable members |
| Immutability / nullability rules | ✅ | ⚠️ immutability | ✅ type- and member-level |
| Dependency rules (on / not on / only on) | ✅ | ✅ | ✅ |
| Body-level (IL) dependency detection | ✅ | ✅ | ⚠️ signature-level default, IL via [custom resolver](../04-configuration.md#dependency-resolver) |
| Dependency cycle detection | ❌ | ✅ via slices | ✅ via namespaces / [slice roots](../03-architecture-rules.md#dependency-cycles) |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Reflection;
using aweXpect.Reflection.Collections;
using aweXpect.Reflection.Helpers;

namespace aweXpect.Reflection;

public static partial class FieldFilters
{
/// <summary>
/// Filters for fields that are required.
/// </summary>
public static Filtered.Fields WhichAreRequired(this Filtered.Fields @this)
=> @this.Which(Filter.Prefix<FieldInfo>(
field => field.IsRequired(),
"required "));

/// <summary>
/// Filters for fields that are not required.
/// </summary>
public static Filtered.Fields WhichAreNotRequired(this Filtered.Fields @this)
=> @this.Which(Filter.Prefix<FieldInfo>(
field => !field.IsRequired(),
"non-required "));
}
11 changes: 11 additions & 0 deletions Source/aweXpect.Reflection/Helpers/FieldInfoHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,15 @@ public static bool HasAttribute(

return false;
}

/// <summary>
/// Checks if the <paramref name="fieldInfo" /> is required (marked with the <c>required</c> modifier).
/// </summary>
/// <remarks>
/// A field is considered required if it carries the
/// <c>System.Runtime.CompilerServices.RequiredMemberAttribute</c>.
/// </remarks>
public static bool IsRequired(this FieldInfo? fieldInfo)
=> fieldInfo != null && fieldInfo.GetCustomAttributes(true)
.Any(attribute => attribute.GetType().FullName == "System.Runtime.CompilerServices.RequiredMemberAttribute");
}
59 changes: 59 additions & 0 deletions Source/aweXpect.Reflection/ThatField.IsRequired.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System.Reflection;
using System.Text;
using aweXpect.Core;
using aweXpect.Core.Constraints;
using aweXpect.Reflection.Helpers;
using aweXpect.Results;

namespace aweXpect.Reflection;

public static partial class ThatField
{
/// <summary>
/// Verifies that the <see cref="FieldInfo" /> is required.
/// </summary>
public static AndOrResult<FieldInfo?, IThat<FieldInfo?>> IsRequired(
this IThat<FieldInfo?> subject)
=> new(subject.Get().ExpectationBuilder.AddConstraint((it, grammars)
=> new IsRequiredConstraint(it, grammars)),
subject);

/// <summary>
/// Verifies that the <see cref="FieldInfo" /> is not required.
/// </summary>
public static AndOrResult<FieldInfo?, IThat<FieldInfo?>> IsNotRequired(
this IThat<FieldInfo?> subject)
=> new(subject.Get().ExpectationBuilder.AddConstraint((it, grammars)
=> new IsRequiredConstraint(it, grammars).Invert()),
subject);

private sealed class IsRequiredConstraint(string it, ExpectationGrammars grammars)
: ConstraintResult.WithNotNullValue<FieldInfo?>(it, grammars),
IValueConstraint<FieldInfo?>
{
public ConstraintResult IsMetBy(FieldInfo? actual)
{
Actual = actual;
Outcome = actual.IsRequired() ? Outcome.Success : Outcome.Failure;
return this;
}

protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null)
=> stringBuilder.Append("is required");

protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null)
{
stringBuilder.Append(It).Append(" was non-required ");
Formatter.Format(stringBuilder, Actual);
}

protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null)
=> stringBuilder.Append("is not required");

protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null)
{
stringBuilder.Append(It).Append(" was required ");
Formatter.Format(stringBuilder, Actual);
}
}
}
128 changes: 128 additions & 0 deletions Source/aweXpect.Reflection/ThatFields.AreRequired.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using aweXpect.Core;
using aweXpect.Core.Constraints;
using aweXpect.Reflection.Helpers;
using aweXpect.Results;
#if NET8_0_OR_GREATER
using System.Threading;
using System.Threading.Tasks;
#endif

// ReSharper disable PossibleMultipleEnumeration

namespace aweXpect.Reflection;

public static partial class ThatFields
{
/// <summary>
/// Verifies that all items in the filtered collection of <see cref="FieldInfo" /> are required.
/// </summary>
public static AndOrResult<IEnumerable<FieldInfo?>, IThat<IEnumerable<FieldInfo?>>> AreRequired(
this IThat<IEnumerable<FieldInfo?>> subject)
=> new(subject.Get().ExpectationBuilder.AddConstraint<IEnumerable<FieldInfo?>>((it, grammars)
=> new AreRequiredConstraint(it, grammars)),
subject);

#if NET8_0_OR_GREATER
/// <summary>
/// Verifies that all items in the filtered collection of <see cref="FieldInfo" /> are required.
/// </summary>
public static AndOrResult<IAsyncEnumerable<FieldInfo?>, IThat<IAsyncEnumerable<FieldInfo?>>> AreRequired(
this IThat<IAsyncEnumerable<FieldInfo?>> subject)
=> new(subject.Get().ExpectationBuilder.AddConstraint<IAsyncEnumerable<FieldInfo?>>((it, grammars)
=> new AreRequiredConstraint(it, grammars)),
subject);
#endif

/// <summary>
/// Verifies that all items in the filtered collection of <see cref="FieldInfo" /> are not required.
/// </summary>
public static AndOrResult<IEnumerable<FieldInfo?>, IThat<IEnumerable<FieldInfo?>>> AreNotRequired(
this IThat<IEnumerable<FieldInfo?>> subject)
=> new(subject.Get().ExpectationBuilder.AddConstraint<IEnumerable<FieldInfo?>>((it, grammars)
=> new AreNotRequiredConstraint(it, grammars)),
subject);

#if NET8_0_OR_GREATER
/// <summary>
/// Verifies that all items in the filtered collection of <see cref="FieldInfo" /> are not required.
/// </summary>
public static AndOrResult<IAsyncEnumerable<FieldInfo?>, IThat<IAsyncEnumerable<FieldInfo?>>> AreNotRequired(
this IThat<IAsyncEnumerable<FieldInfo?>> subject)
=> new(subject.Get().ExpectationBuilder.AddConstraint<IAsyncEnumerable<FieldInfo?>>((it, grammars)
=> new AreNotRequiredConstraint(it, grammars)),
subject);
#endif

private sealed class AreRequiredConstraint(string it, ExpectationGrammars grammars)
: CollectionConstraintResult<FieldInfo?>(grammars),
IValueConstraint<IEnumerable<FieldInfo?>>
#if NET8_0_OR_GREATER
, IAsyncConstraint<IAsyncEnumerable<FieldInfo?>>
#endif
{
#if NET8_0_OR_GREATER
public async Task<ConstraintResult> IsMetBy(IAsyncEnumerable<FieldInfo?> actual,
CancellationToken cancellationToken)
=> await SetAsyncValue(actual, field => field.IsRequired());
#endif

public ConstraintResult IsMetBy(IEnumerable<FieldInfo?> actual)
=> SetValue(actual, field => field.IsRequired());

protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null)
=> stringBuilder.Append("are all required");

protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null)
{
stringBuilder.Append(it).Append(" contained non-required fields ");
Formatter.Format(stringBuilder, NotMatching, FormattingOptions.Indented(indentation));
}

protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null)
=> stringBuilder.Append("are not all required");

protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null)
{
stringBuilder.Append(it).Append(" only contained required fields ");
Formatter.Format(stringBuilder, Matching, FormattingOptions.Indented(indentation));
}
}

private sealed class AreNotRequiredConstraint(string it, ExpectationGrammars grammars)
: CollectionConstraintResult<FieldInfo?>(grammars),
IValueConstraint<IEnumerable<FieldInfo?>>
#if NET8_0_OR_GREATER
, IAsyncConstraint<IAsyncEnumerable<FieldInfo?>>
#endif
{
#if NET8_0_OR_GREATER
public async Task<ConstraintResult> IsMetBy(IAsyncEnumerable<FieldInfo?> actual,
CancellationToken cancellationToken)
=> await SetAsyncValue(actual, field => !field.IsRequired());
#endif

public ConstraintResult IsMetBy(IEnumerable<FieldInfo?> actual)
=> SetValue(actual, field => !field.IsRequired());

protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null)
=> stringBuilder.Append("are all not required");

protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null)
{
stringBuilder.Append(it).Append(" contained required fields ");
Formatter.Format(stringBuilder, NotMatching, FormattingOptions.Indented(indentation));
}

protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null)
=> stringBuilder.Append("also contain a required field");

protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null)
{
stringBuilder.Append(it).Append(" only contained non-required fields ");
Formatter.Format(stringBuilder, Matching, FormattingOptions.Indented(indentation));
}
}
}
Loading
Loading