Skip to content

Conversation

@Notallthatevil
Copy link
Contributor

@Notallthatevil Notallthatevil commented Aug 15, 2025

When a struct had an empty constructor that modified a field/property to a non-default value. The matcher would fail and throw a runtime error. This bug fix attempts to fix that issue by essentially zero initializing the struct the same way that default(T) does through the use of a runtime helper method. Simply, we replace Activator.CreateInstance with GetUninitializedObject.

The idea came from this post

Which contains and deprecated method as of .NET8. Microsoft has a recommended fix for that here.

This resolves the issue in #766

…time error.

When a struct had an empty constructor that modified a field/property to a non-default value. The matcher would fail and throw a runtime error. This bug fix attempts to fix that issue by essentially zero initializing the struct the same way that default(T) does through the use of a runtime helper method. Simply, we replace Activator.CreateInstance with GetUninitializedObject.

The idea came from this post
https://stackoverflow.com/questions/1005336/c-sharp-using-reflection-to-create-a-struct

Which contains and deprecated method as of .NET8. Microsoft has a recommended fix for that here.
https://learn.microsoft.com/en-us/dotnet/fundamentals/syslib-diagnostics/syslib0050
@304NotModified
Copy link
Contributor

Thanks for the pull request!

Could you please add some unit tests?

- Added test method `Any_on_struct_with_default_constructor_should_work` to ensure method calls with struct arguments do not throw exceptions.
- Introduced `StructWithDefaultConstructor` struct:
  - Contains a property `Value`.
  - Has a default constructor initializing `Value` to 42.
- Created `IWithStructWithDefaultConstructor` interface:
  - Declares `MethodWithStruct` that accepts `StructWithDefaultConstructor` as an argument.
@Notallthatevil
Copy link
Contributor Author

I added a simple test that does fail on the main branch. Not sure there is a whole lot to test here. I did ensure that all tests pass, especially ones that take in nullable primitive types like int? as they need special handling with the new DefaultInstanceOfValueType functionality. If you have any specific scenarios you would like to see let me know.

@Notallthatevil
Copy link
Contributor Author

Is there anything still blocking this from being merged?

@Notallthatevil
Copy link
Contributor Author

I fixed the format issue.

@304NotModified
Copy link
Contributor

304NotModified commented Sep 4, 2025

Is there anything still blocking this from being merged?

Only a review I guess 😅

return BoxedDouble;
}

return Activator.CreateInstance(returnType)!;
Copy link
Contributor

@304NotModified 304NotModified Sep 4, 2025

Choose a reason for hiding this comment

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

I'm doubting if we should use Activator.CreateInstance(returnType)!; for non-stucts, to keep things 100% the same for the previous cases.

Although all tests works, so not sure.

@nsubstitute/core-team any opinion on this?

Copy link
Member

Choose a reason for hiding this comment

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

I'd feel more comfortable if we keep the existing behaviour for non-structs. Never sure how reflection stuff is going to break 😅

Copy link
Contributor

Choose a reason for hiding this comment

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

Is this fixed? This isn't marked as resolved?

Copy link
Contributor

@304NotModified 304NotModified Dec 12, 2025

Choose a reason for hiding this comment

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

So after some reading, this is large change?

 Activator.CreateInstance(returnType)!;

Calls the (default) constructor - that is indeed an issue with structs

System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject

Won't call the constructor.

I'm not sure if this is a good idea.

I'm not sure how DefaultValue is used exactly and if this poses issues.

Anyway, without resolving this discussion, I cannot approve this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The method itself is attempting to get the default value for the type. i.e. 0 for int. For reference types, as you know, this would return null when you try to do object o = default;.

For structs, when you set them to default, for example, when the parameter list has something along the lines of void MyMethod(CancellationToken cancellationToken = default) and then you try to create a matcher against it using Arg.Any<CancellationToken>(), it results in two instances of a struct that will not match, because Arg.Any<CancellationToken>() does not equal default(CancellationToken), which is why GetUninitializedObject is needed.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR fixes a runtime error that occurred when using NSubstitute's Arg.Any<T>() matcher with structs that have non-default constructors. The issue arose because Activator.CreateInstance would invoke the constructor, causing the matcher to fail when comparing against the modified struct values.

  • Replaces Activator.CreateInstance with GetUninitializedObject to create zero-initialized structs
  • Adds special handling for Nullable<T> types to return null
  • Implements conditional compilation for .NET 5+ vs older frameworks

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/NSubstitute/Core/DefaultForType.cs Updates struct instantiation logic to use GetUninitializedObject instead of Activator.CreateInstance
tests/NSubstitute.Acceptance.Specs/ArgumentMatching.cs Adds regression test for struct with default constructor scenario

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Updated `GetDefault` methods in `AutoObservableProvider.cs` and `AutoTaskProvider.cs` to use the `DefaultForType` class for determining default values, improving code modularity and reusability.
@Notallthatevil
Copy link
Contributor Author

@304NotModified Anything still holding this back that I can help with? I would love to not have to maintain my own branch anymore and it seems that others have faced problems similar to this one.

Copy link
Member

@dtchepak dtchepak left a comment

Choose a reason for hiding this comment

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

LGTM, thanks!

@Romfos, @304NotModified , if this looks ok to use please approve and merge 🙏

@Romfos
Copy link
Contributor

Romfos commented Nov 13, 2025

cc @304NotModified

@Notallthatevil
Copy link
Contributor Author

@304NotModified I am sure you are busy, is there anything I can help with that is holding you back from approving these changes?

@304NotModified
Copy link
Contributor

Hey!

I'm not that active anymore on NSubstitute 😅
And I got hunderds of notifications/tags recently, so dropped a lot of those notifications 😅

I could try to check this PR this week, but I'm not sure if this needs an approval of me?

@304NotModified 304NotModified self-assigned this Dec 8, 2025
@nsubstitute nsubstitute deleted a comment from Copilot AI Dec 12, 2025
@nsubstitute nsubstitute deleted a comment from Copilot AI Dec 12, 2025
@304NotModified
Copy link
Contributor

304NotModified commented Dec 12, 2025

reviewed. There is still an open discussion in this PR. I don't know the exact side effects of this change, so unfortunately I cannot approve this.

@dtchepak maybe enable the check of the discussions so we could not forget this

Under settings -> rules -> rulesets

image

@304NotModified 304NotModified removed their assignment Dec 12, 2025
Copy link
Contributor

@zvirja zvirja left a comment

Choose a reason for hiding this comment

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

Looks reasonable to me! I think the usage of Activator was indeed meant to get uninitialized zeroed struct. Especially giving that it's exactly what default() is doing and is not invoking struct constructor.

@304NotModified 304NotModified merged commit 6f75ebc into nsubstitute:main Dec 26, 2025
4 checks passed
@304NotModified
Copy link
Contributor

304NotModified commented Dec 26, 2025

Ok let's merge then! :)

@Notallthatevil Notallthatevil deleted the 766-Argument-Matcher-For-A-Struct branch December 26, 2025 00:20
@Romfos Romfos mentioned this pull request Dec 26, 2025
26 tasks
@dtchepak
Copy link
Member

dtchepak commented Jan 9, 2026

@dtchepak maybe enable the check of the discussions so we could not forget this

Under settings -> rules -> rulesets

Thanks @304NotModified . Previously PR rules were applied via branch protection. I've added a ruleset for default branch as well so hopefully this will be sorted now. 🤞

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.

5 participants