Add compile-time property initializer for default values in BindableProperty source generaor#3204
Merged
TheCodeTraveler merged 14 commits intoCommunityToolkit:mainfrom Apr 19, 2026
Conversation
…roperty source generaor
6 tasks
Contributor
There was a problem hiding this comment.
Pull request overview
This PR updates the BindableProperty source generator to avoid emitting defaultValueCreator delegates when a property’s initializer can be resolved into a direct defaultValue expression, addressing race conditions seen in multi-threaded test runs. It also includes a couple of test-stability fixes in the toolkit.
Changes:
- Added an initializer-expression resolver and updated the BindableProperty generator/model to emit
defaultValue:directly when possible. - Updated generator unit tests to reflect the new generated output.
- Removed
.ConfigureAwait(false)fromasync voidbehavior hooks (and added suppression logging), plus a small PopupService unit test logging fix.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/CommunityToolkit.Maui/Behaviors/Validators/ValidationBehavior.shared.cs | Removes .ConfigureAwait(false) from async void hooks; adds guarded exception suppression/logging. |
| src/CommunityToolkit.Maui.UnitTests/Services/PopupServiceTests.cs | Makes debug logging resilient when Application.Current.Windows is empty. |
| src/CommunityToolkit.Maui.SourceGenerators/Models/BindablePropertyRecords.cs | Extends the model to carry a resolved initializer expression and adjusts decision flags. |
| src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs | New helper to resolve initializer syntax into emitted C# expressions for defaultValue:. |
| src/CommunityToolkit.Maui.SourceGenerators/Generators/BindablePropertyAttributeSourceGenerator.cs | Emits resolved defaultValue: when available and threads it through semantic transform/model creation. |
| src/CommunityToolkit.Maui.SourceGenerators.UnitTests/...IntegrationTests.cs | Updates expected generator output to match new defaultValue: emission behavior. |
| src/CommunityToolkit.Maui.SourceGenerators.UnitTests/...EdgeCaseTests.cs | Updates expected output for more initializer shapes (including UtcNow, TimeSpan.FromSeconds, etc). |
| src/CommunityToolkit.Maui.SourceGenerators.UnitTests/...CommonUsageTests.cs | Updates expected output to use direct defaultValue: where resolvable. |
…ExpressionResolver.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…ExpressionResolver.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…ExpressionResolver.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…ExpressionResolver.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
.github/instructions/codestyle.instructions.md:582
- The added "Try Pattern" section starts a markdown code fence but never closes it (the file ends immediately after the opening
). This will break rendering for the rest of the document; either remove the stray fence or add a complete example and closing.
### Try Pattern
When a method name begins with `Try` and returns a variable, ensure it adheres to the Try Pattern.
Here Key Components of the Try Pattern:
1. Method Name: Begins with Try (e.g., TryParse, TryGetValue).
2. Return Type: bool (indicating success or failure).
3. Out Parameter: An out parameter to return the result if successful.
4. Null Analysis Attribute: [NotNullWhen(true)] (or [MaybeNullWhen(false)]) informs the compiler that if the method returns true, the out parameter is guaranteed to be non-null.
This pattern allows for high-performance retrieval or parsing without throwing exceptions for expected failures. It also allows cleaner call sites by eliminating the need for null-checking the output variable within the if block, as seen in Dictionary<TKey, TValue>.TryGetValue.
</details>
…ExpressionResolver.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…tializerExpressionResolver.cs" This reverts commit 375870b.
TheCodeTraveler
approved these changes
Apr 19, 2026
Collaborator
TheCodeTraveler
left a comment
There was a problem hiding this comment.
You're a legend! Thanks Matt!!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description of Change
The source generator wraps all property initializers in a
defaultValueCreatordelegate. This causes intermittent failures in unit tests:This could also theoretically surface in production code with concurrent property access, which is highly unlikely as test context differs in some important ways (specifically having a UI thread/synchronization context), but possible.
Root cause:
This was quite hard to track down.
BindableProperty.CreatewithdefaultValueCreator:uses a lazy-initialization path, that eventually hitsBindableObject.GetValue()that performs a dictionary write on first access.Dictionaryis not threadsafe, and when multiple threads access the property concurrently before it has been initialized, the dictionary write races and throws. Hand-written BindableProperties usingdefaultValue:follow a pure-read path and never hit this race.I note that there were some protections against this in the code already (
volatileon the init flag), but this wasn't working as expected. The documentation for thevolatilekeyword notes that the affected field is excluded from certain optimizations to help with concurrency, but warnings highlight that higher-level alternatives are recommended, and specifically:Presumably this is the scenario encountered during tests.
Two other changes attempted to address this:
ConfigureAwait(false): this is correct as in UI bound operations the bindable property has thread affinity, and the operation should always return to the caller, but while this seemed to help, it didn't resolve the issue.[ThreadStatic]to the initialized flag: appeared to resolve the problem at first and had a bigger impact, but was not reliably consistent. As mentioned above, eventually we hit the internal values dictionary inBindableObject.GetValue()and there's no way around this if usingdefaultValueCreator.Solution:
The fundamental problem is using static values in bindable property generators in multi-threaded scenarios (as noted in #3096, see below linked issues). This was required in some scenarios (and still is, the current approach is retained as a fallback), but it's also possible to infer the default value at compile time.
A new resolver/helper parses the initializer expression and generates fully-qualified C# code at compile time. This completely eliminates the issue as the code path that hits the
BindableObject.GetValuemethod at runtime never encounters an empty value. The bindable properties in the toolkit have been updated to use this, which resolves all build issues. I've successfully re-run this several times with no failed builds.Summary of changes:
New
InitializerExpressionResolver: This is the core of the change. A helper resolves property initializer expressions (literals, enum members,default,new T(), static field/property references, casts, etc.) to fully-qualified C# code at compile time. Unresolvable expressions (collection expressions[], lambdas, complex method calls) returnnulland fall back to the existingdefaultValueCreatorpath.Updated
BindablePropertyAttributeSourceGenerator: When the resolver succeeds, emits the resolved expression directly as thedefaultValue:parameter instead of wrapping it in adefaultValueCreatordelegate. When it cannot resolve (or when the user explicitly setsDefaultValueCreatorMethodName), behavior is unchanged.ValidationBehavior fix: Removed
.ConfigureAwait(false)from threeasync voidoverride methods (OnAttachedTo,OnViewPropertyChanged,OnValidationPropertyChanged). Inasync voidmethods,.ConfigureAwait(false)can cause continuations to run on thread pool threads, leading to race conditions in test environments without aSynchronizationContext. Added try/catch toOnValidationPropertyChangedwithOptions.ShouldSuppressExceptionsInBehaviorsguard.PopupServiceTests fix: Changed
Windows[0]toWindows.FirstOrDefault()in debug logging to preventArgumentOutOfRangeExceptionwhen no windows exist during test teardown. (Unrelated issue but was the only thing blocking a clean build so I added this).Linked Issues
BindablePropertyDefaultValueCreatorAnalyzerto to avoid Static References from being passed intoCreateDefaultValueMethodNamewhen using[BindableProperty]and[AttachedBindableProperty<T>]. #3096 which adds an analyzer (MCT003) warning whenDefaultValueCreatorMethodNameunnecessarily wraps a static value. This PR addresses the same underlying problem from the generator side, preventing unnecessarydefaultValueCreatordelegates from being emitted in the first place.PR Checklist
approved(bug) orChampioned(feature/proposal)mainat time of PRAdditional information