Skip to content

Fix nullable warnings when using Member() on nullable properties#5191

Merged
thomhurst merged 3 commits intomainfrom
copilot/fix-nullable-string-warnings
Mar 21, 2026
Merged

Fix nullable warnings when using Member() on nullable properties#5191
thomhurst merged 3 commits intomainfrom
copilot/fix-nullable-string-warnings

Conversation

Copy link
Contributor

Copilot AI commented Mar 19, 2026

Member(x => x.Schema, schema => schema.IsEqualTo("dbo")) on a string? property produces CS8603 because the compiler infers TMember = string but the selector lambda returns string?, which doesn't match Expression<Func<TObject, string>>.

Changed the selector parameter from Expression<Func<TObject, TMember>> to Expression<Func<TObject, TMember?>> in:

  • AssertionExtensions.cs — all 3 Member overloads (<TObject, TMember, TTransformed>, <TObject, TMember> strongly-typed, <TObject, TMember> object-returning)
  • PropertyAssertionExtensions.cs — both HasProperty overloads (delegates to Member internally)

With TMember? in the selector, the compiler still infers TMember = string while accepting a string? return. The assertions lambda stays Func<IAssertionSource<TMember>, Assertion<TMember>> — i.e. Func<IAssertionSource<string>, Assertion<string>> — which matches StringEqualsAssertion : Assertion<string> without any nullability mismatch.

sealed class Model { public string? Schema { get; set; } }

// Both now compile warning-free:
await Assert.That(model)
    .Member(x => x.Schema, schema => schema.IsEqualTo("dbo"));

await Assert.That(model)
    .HasProperty(x => x.Schema, "dbo");
Original prompt

This section details on the original issue you should resolve

<issue_title>[Bug]: Member(...) on nullable string property produces nullable warnings</issue_title>
<issue_description>### Description

Using TUnit 1.19.74, Member(...) on a property declared as string? produces nullable warnings. The warnings appear when asserting on the nullable member with both inferred and explicit generic arguments (although you get different warnings in both cases).

Expected Behavior

Neither of these calls produces a warning.

Actual Behavior

The warnings occur on these specific lines:

await Assert.That(model)
    .Member(x => x.Schema, schema => schema.IsEqualTo("dbo"));

Produces:
CS8603: Possible null reference return. - this points to the selector lambda x => x.Schema.

await Assert.That(model)
    .Member<Model, string?>(x => x.Schema, schema => schema.IsEqualTo("dbo"));

Produces:
CS8620: Argument of type 'IAssertionSource<string?>' cannot be used for parameter 'source' of type 'IAssertionSource<string>' ...
CS8619: Nullability of reference types in value of type 'StringEqualsAssertion' doesn't match target type 'Assertion<string?>'.

Steps to Reproduce

Use the following minimal test:

#:package TUnit@1.19.74

#nullable enable
using TUnit.Assertions;

public class Tests
{
    [Test]
    public async Task Test()
    {
        var model = new Model
        {
            Schema = "dbo"
        };

        await Assert.That(model)
            .Member(x => x.Schema, schema => schema.IsEqualTo("dbo"));

        await Assert.That(model)
            .Member<Model, string?>(x => x.Schema, schema => schema.IsEqualTo("dbo"));
    }
}

sealed class Model
{
    public string? Schema { get; set; }
}

TUnit Version

1.19.74

.NET Version

.NET10

Operating System

macOS

IDE / Test Runner

JetBrains Rider

Error Output / Stack Trace

Additional Context

No response

IDE-Specific Issue?

  • I've confirmed this issue occurs when running via dotnet test or dotnet run, not just in my IDE</issue_description>

Comments on the Issue (you are @copilot in this section)


💬 Send tasks to Copilot coding agent from Slack and Teams to turn conversations into code. Copilot posts an update in your thread when it's finished.

…lable warnings

Change Expression<Func<TObject, TMember>> to Expression<Func<TObject, TMember?>>
in Member and HasProperty overloads so that selectors on nullable properties
(e.g. string?) no longer produce CS8603/CS8620/CS8619 warnings.

Co-authored-by: thomhurst <30480171+thomhurst@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix nullable warnings on Member() for string properties Fix nullable warnings when using Member() on nullable properties Mar 19, 2026
Copilot AI requested a review from thomhurst March 19, 2026 14:18
Copilot AI temporarily deployed to Pull Requests March 19, 2026 14:40 Inactive
@thomhurst
Copy link
Owner

@copilot can we make sure we have sufficient test coverage for:
Reference types
Non null structs
Nullable structs

And any other you can think of

…tructs, nullable structs, enums, collections, and chaining

Co-authored-by: thomhurst <30480171+thomhurst@users.noreply.github.com>
Agent-Logs-Url: https://github.com/thomhurst/TUnit/sessions/abcf6a00-83b2-4c2d-be1a-acf22a4cf702
Copy link
Contributor Author

Copilot AI commented Mar 21, 2026

@copilot can we make sure we have sufficient test coverage for:
Reference types
Non null structs
...

Added MemberNullabilityTests in commit 0d2dca9 with 44 tests covering:

  • Non-nullable reference types: string, object, custom class, List<int>
  • Nullable reference types: string?, object?, custom class?, List<int>? (with both value and null cases)
  • Non-nullable structs: int, bool, DateTime, custom struct, enum
  • Nullable structs: int?, bool?, DateTime?, custom struct?, enum? (with both value and null cases)
  • Chaining: mixed nullable/non-nullable types in .And.Member() chains
  • HasProperty: value syntax, fluent syntax (.IsNull()/.IsNotNull()), and chained mixed types

Copilot AI temporarily deployed to Pull Requests March 21, 2026 12:05 Inactive
Copilot AI temporarily deployed to Pull Requests March 21, 2026 12:05 Inactive
@thomhurst thomhurst marked this pull request as ready for review March 21, 2026 12:05
@thomhurst thomhurst enabled auto-merge (squash) March 21, 2026 12:06
Copy link
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: Fix nullable warnings when using Member() on nullable properties

Overall: Clean, minimal fix for a real pain point. The approach is correct and the test coverage is excellent.

The Fix

Changing the selector parameter from Expression<Func<TObject, TMember>> to Expression<Func<TObject, TMember?>> is the right solution here. It exploits how C# type inference works:

  • For reference types (string?): the compiler still infers TMember = string, accepts the nullable selector, and the assertions lambda remains typed to IAssertionSource<string> — eliminating CS8603.
  • For value types (int?): the compiler infers TMember = int from the Nullable<int>TMember? unification, keeping assertions typed to IAssertionSource<int>.

This is backwards-compatible: existing callers with non-nullable properties continue to work unchanged.

Test Coverage

The 30+ test cases covering reference types, value types, structs, enums, nullable variants, chaining, and HasProperty are comprehensive. The explicit "Failure" cases (Member_NonNullableString_Failure, Member_NonNullableInt_Failure) that verify the assertion message are a nice touch.

Pre-existing Performance Issue (not introduced here, but worth flagging)

Inside the Map lambda in all three overloads, memberSelector.Compile() is called on each evaluation:

var memberContext = parentContext.Map<TMember>(obj =>
{
    if (obj == null) throw ...;
    var compiled = memberSelector.Compile();  // recompiles every time Map runs
    return compiled(obj);
});

Expression compilation is expensive. This should be hoisted outside the lambda:

var compiled = memberSelector.Compile();  // compile once
var memberContext = parentContext.Map<TMember>(obj =>
{
    if (obj == null) throw ...;
    return compiled(obj);
});

This applies to all 5 methods touched in this PR (3 in AssertionExtensions.cs, 2 in PropertyAssertionExtensions.cs). Given TUnit's performance-first philosophy, this is worth addressing — though it predates this PR.

Minor Gap

There's no test for HasProperty(x => x.NullableName, null) (asserting a property is null via the expected-value overload). Not a blocker, just a potential edge case to consider documenting or testing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Member(...) on nullable string property produces nullable warnings

2 participants