From a9421dfea7c0dddef84639f0dd5ffd80d2406612 Mon Sep 17 00:00:00 2001 From: Matt Goldman Date: Mon, 6 Apr 2026 21:33:04 +1000 Subject: [PATCH 1/8] Fix: [THREADSAFE] Use ThreadStatic for initializing property flag to prevent cross-thread interference --- .../BindablePropertyAttributeSourceGenerator.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/CommunityToolkit.Maui.SourceGenerators/Generators/BindablePropertyAttributeSourceGenerator.cs b/src/CommunityToolkit.Maui.SourceGenerators/Generators/BindablePropertyAttributeSourceGenerator.cs index b7d163b3a2..7b806c88a0 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators/Generators/BindablePropertyAttributeSourceGenerator.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators/Generators/BindablePropertyAttributeSourceGenerator.cs @@ -597,15 +597,16 @@ static string GetFormattedReturnType(ITypeSymbol typeSymbol) /// Property model. static void AppendHelperInitializingField(StringBuilder fileStaticClassStringBuilder, in BindablePropertyModel info) { - // Make the flag public static so it can be referenced from the generated partial class in the same file. - fileStaticClassStringBuilder.Append("public static volatile bool ") + // Use [ThreadStatic] so each thread has its own flag, preventing cross-thread + // interference when multiple instances resolve default values concurrently. + fileStaticClassStringBuilder.Append("[global::System.ThreadStatic]\npublic static bool ") .Append(info.InitializingPropertyName) - .Append(" = false;\n"); + .Append(";\n"); } /// /// Appends a default value creator method into the file-static helper class. - /// The method sets the static initializing flag, reads the property's initializer value by casting the bindable + /// The method sets the thread-static initializing flag, reads the property's initializer value by casting the bindable /// to the declaring type, then clears the flag and returns the value. /// /// Helper StringBuilder used to collect helper members. @@ -621,14 +622,17 @@ static void AppendHelperDefaultValueMethod(StringBuilder fileStaticClassStringBu .Append("{\n") .Append(info.InitializingPropertyName) .Append(" = true;\n") + .Append("try\n{\n") .Append("var defaultValue = ((") .Append(fullDeclaringType) .Append(")bindable).") .Append(sanitizedPropertyName) .Append(";\n") + .Append("return defaultValue;\n") + .Append("}\nfinally\n{\n") .Append(info.InitializingPropertyName) .Append(" = false;\n") - .Append("return defaultValue;\n") + .Append("}\n") .Append("}\n\n"); } } \ No newline at end of file From 3e7873852e2924e0d3217c31b35db5f9a894ad6c Mon Sep 17 00:00:00 2001 From: Matt Goldman Date: Tue, 7 Apr 2026 05:21:51 +1000 Subject: [PATCH 2/8] Use ThreadStatic for initialization flags in BindableProperty tests to prevent cross-thread interference --- ...tributeSourceGenerator_CommonUsageTests.cs | 30 ++- ...yAttributeSourceGenerator_EdgeCaseTests.cs | 180 +++++++++++++----- ...tributeSourceGenerator_IntegrationTests.cs | 15 +- 3 files changed, 165 insertions(+), 60 deletions(-) diff --git a/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_CommonUsageTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_CommonUsageTests.cs index 9b73716179..9958053925 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_CommonUsageTests.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_CommonUsageTests.cs @@ -610,22 +610,36 @@ public partial class {{defaultTestClassName}} file static class __{{defaultTestClassName}}BindablePropertyInitHelpers { - public static volatile bool IsInitializingText = false; + [global::System.ThreadStatic] + public static bool IsInitializingText; public static object CreateDefaultText(global::Microsoft.Maui.Controls.BindableObject bindable) { IsInitializingText = true; - var defaultValue = ((TestView)bindable).Text; - IsInitializingText = false; - return defaultValue; + try + { + var defaultValue = ((TestView)bindable).Text; + return defaultValue; + } + finally + { + IsInitializingText = false; + } } - public static volatile bool IsInitializingCustomDuration = false; + [global::System.ThreadStatic] + public static bool IsInitializingCustomDuration; public static object CreateDefaultCustomDuration(global::Microsoft.Maui.Controls.BindableObject bindable) { IsInitializingCustomDuration = true; - var defaultValue = ((TestView)bindable).CustomDuration; - IsInitializingCustomDuration = false; - return defaultValue; + try + { + var defaultValue = ((TestView)bindable).CustomDuration; + return defaultValue; + } + finally + { + IsInitializingCustomDuration = false; + } } } """; diff --git a/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_EdgeCaseTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_EdgeCaseTests.cs index 951ffe92a1..928ed38299 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_EdgeCaseTests.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_EdgeCaseTests.cs @@ -100,13 +100,20 @@ public partial class {{defaultTestClassName}} file static class __{{defaultTestClassName}}BindablePropertyInitHelpers { - public static volatile bool IsInitializingInvoiceStatus = false; + [global::System.ThreadStatic] + public static bool IsInitializingInvoiceStatus; public static object CreateDefaultInvoiceStatus(global::Microsoft.Maui.Controls.BindableObject bindable) { IsInitializingInvoiceStatus = true; - var defaultValue = ((TestView)bindable).InvoiceStatus; - IsInitializingInvoiceStatus = false; - return defaultValue; + try + { + var defaultValue = ((TestView)bindable).InvoiceStatus; + return defaultValue; + } + finally + { + IsInitializingInvoiceStatus = false; + } } } """; @@ -160,13 +167,20 @@ public partial class {{defaultTestClassName}} file static class __{{defaultTestClassName}}BindablePropertyInitHelpers { - public static volatile bool IsInitializingInvoiceStatus = false; + [global::System.ThreadStatic] + public static bool IsInitializingInvoiceStatus; public static object CreateDefaultInvoiceStatus(global::Microsoft.Maui.Controls.BindableObject bindable) { IsInitializingInvoiceStatus = true; - var defaultValue = ((TestView)bindable).InvoiceStatus; - IsInitializingInvoiceStatus = false; - return defaultValue; + try + { + var defaultValue = ((TestView)bindable).InvoiceStatus; + return defaultValue; + } + finally + { + IsInitializingInvoiceStatus = false; + } } } """; @@ -516,67 +530,116 @@ public partial class {{defaultTestClassName}} file static class __{{defaultTestClassName}}BindablePropertyInitHelpers { - public static volatile bool IsInitializingIsEnabled = false; + [global::System.ThreadStatic] + public static bool IsInitializingIsEnabled; public static object CreateDefaultIsEnabled(global::Microsoft.Maui.Controls.BindableObject bindable) { IsInitializingIsEnabled = true; - var defaultValue = ((TestView)bindable).IsEnabled; - IsInitializingIsEnabled = false; - return defaultValue; + try + { + var defaultValue = ((TestView)bindable).IsEnabled; + return defaultValue; + } + finally + { + IsInitializingIsEnabled = false; + } } - public static volatile bool IsInitializingPi = false; + [global::System.ThreadStatic] + public static bool IsInitializingPi; public static object CreateDefaultPi(global::Microsoft.Maui.Controls.BindableObject bindable) { IsInitializingPi = true; - var defaultValue = ((TestView)bindable).Pi; - IsInitializingPi = false; - return defaultValue; + try + { + var defaultValue = ((TestView)bindable).Pi; + return defaultValue; + } + finally + { + IsInitializingPi = false; + } } - public static volatile bool IsInitializingLetter = false; + [global::System.ThreadStatic] + public static bool IsInitializingLetter; public static object CreateDefaultLetter(global::Microsoft.Maui.Controls.BindableObject bindable) { IsInitializingLetter = true; - var defaultValue = ((TestView)bindable).Letter; - IsInitializingLetter = false; - return defaultValue; + try + { + var defaultValue = ((TestView)bindable).Letter; + return defaultValue; + } + finally + { + IsInitializingLetter = false; + } } - public static volatile bool IsInitializingTimeSpent = false; + [global::System.ThreadStatic] + public static bool IsInitializingTimeSpent; public static object CreateDefaultTimeSpent(global::Microsoft.Maui.Controls.BindableObject bindable) { IsInitializingTimeSpent = true; - var defaultValue = ((TestView)bindable).TimeSpent; - IsInitializingTimeSpent = false; - return defaultValue; + try + { + var defaultValue = ((TestView)bindable).TimeSpent; + return defaultValue; + } + finally + { + IsInitializingTimeSpent = false; + } } - public static volatile bool IsInitializingDoubleEpsilon = false; + [global::System.ThreadStatic] + public static bool IsInitializingDoubleEpsilon; public static object CreateDefaultDoubleEpsilon(global::Microsoft.Maui.Controls.BindableObject bindable) { IsInitializingDoubleEpsilon = true; - var defaultValue = ((TestView)bindable).DoubleEpsilon; - IsInitializingDoubleEpsilon = false; - return defaultValue; + try + { + var defaultValue = ((TestView)bindable).DoubleEpsilon; + return defaultValue; + } + finally + { + IsInitializingDoubleEpsilon = false; + } } - public static volatile bool IsInitializingSingleEpsilon = false; + [global::System.ThreadStatic] + public static bool IsInitializingSingleEpsilon; public static object CreateDefaultSingleEpsilon(global::Microsoft.Maui.Controls.BindableObject bindable) { IsInitializingSingleEpsilon = true; - var defaultValue = ((TestView)bindable).SingleEpsilon; - IsInitializingSingleEpsilon = false; - return defaultValue; + try + { + var defaultValue = ((TestView)bindable).SingleEpsilon; + return defaultValue; + } + finally + { + IsInitializingSingleEpsilon = false; + } } - public static volatile bool IsInitializingCurrentTime = false; + [global::System.ThreadStatic] + public static bool IsInitializingCurrentTime; public static object CreateDefaultCurrentTime(global::Microsoft.Maui.Controls.BindableObject bindable) { IsInitializingCurrentTime = true; - var defaultValue = ((TestView)bindable).CurrentTime; - IsInitializingCurrentTime = false; - return defaultValue; + try + { + var defaultValue = ((TestView)bindable).CurrentTime; + return defaultValue; + } + finally + { + IsInitializingCurrentTime = false; + } } } """; @@ -690,31 +753,52 @@ public partial class {{defaultTestClassName}} file static class __{{defaultTestClassName}}BindablePropertyInitHelpers { - public static volatile bool IsInitializingText = false; + [global::System.ThreadStatic] + public static bool IsInitializingText; public static object CreateDefaultText(global::Microsoft.Maui.Controls.BindableObject bindable) { IsInitializingText = true; - var defaultValue = ((TestView)bindable).Text; - IsInitializingText = false; - return defaultValue; + try + { + var defaultValue = ((TestView)bindable).Text; + return defaultValue; + } + finally + { + IsInitializingText = false; + } } - public static volatile bool IsInitializingTime = false; + [global::System.ThreadStatic] + public static bool IsInitializingTime; public static object CreateDefaultTime(global::Microsoft.Maui.Controls.BindableObject bindable) { IsInitializingTime = true; - var defaultValue = ((TestView)bindable).Time; - IsInitializingTime = false; - return defaultValue; + try + { + var defaultValue = ((TestView)bindable).Time; + return defaultValue; + } + finally + { + IsInitializingTime = false; + } } - public static volatile bool IsInitializingCustomDuration = false; + [global::System.ThreadStatic] + public static bool IsInitializingCustomDuration; public static object CreateDefaultCustomDuration(global::Microsoft.Maui.Controls.BindableObject bindable) { IsInitializingCustomDuration = true; - var defaultValue = ((TestView)bindable).CustomDuration; - IsInitializingCustomDuration = false; - return defaultValue; + try + { + var defaultValue = ((TestView)bindable).CustomDuration; + return defaultValue; + } + finally + { + IsInitializingCustomDuration = false; + } } } """; diff --git a/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_IntegrationTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_IntegrationTests.cs index 5f47781e03..4891bc4fc4 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_IntegrationTests.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_IntegrationTests.cs @@ -168,13 +168,20 @@ public partial class {{defaultTestClassName}} [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] private static class __{{defaultTestClassName}}BindablePropertyInitHelpers { - public static volatile bool IsInitializingValue = false; + [global::System.ThreadStatic] + public static bool IsInitializingValue; public static object CreateDefaultValue(global::Microsoft.Maui.Controls.BindableObject bindable) { IsInitializingValue = true; - var defaultValue = (({{defaultTestClassName}})bindable).Value; - IsInitializingValue = false; - return defaultValue; + try + { + var defaultValue = (({{defaultTestClassName}})bindable).Value; + return defaultValue; + } + finally + { + IsInitializingValue = false; + } } } } From 93f1f3634a1b7c10d94770b70295e00082940839 Mon Sep 17 00:00:00 2001 From: Matt Goldman Date: Tue, 7 Apr 2026 07:10:46 +1000 Subject: [PATCH 3/8] Separate build and run steps in benchmarks to prevent parallel race conditions and DLL locks --- .github/workflows/benchmarks.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index de387fc627..93b6f3108d 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -49,11 +49,16 @@ jobs: - name: Display dotnet info run: dotnet --info + - name: Build Benchmarks + run: | + dotnet build ${{ env.PathToCommunityToolkitAnalyzersBenchmarksCsproj }} -c Release + dotnet build ${{ env.PathToCommunityToolkitSourceGeneratorsBenchmarksCsproj }} -c Release + - name: Run Analyzer Benchmarks - run: dotnet run --project ${{ env.PathToCommunityToolkitAnalyzersBenchmarksCsproj }} -c Release -- -a "${{ runner.temp }}" + run: dotnet run --project ${{ env.PathToCommunityToolkitAnalyzersBenchmarksCsproj }} -c Release --no-build -- -a "${{ runner.temp }}" - name: Run Source Generator Benchmarks - run: dotnet run --project ${{ env.PathToCommunityToolkitSourceGeneratorsBenchmarksCsproj }} -c Release -- -a "${{ runner.temp }}" + run: dotnet run --project ${{ env.PathToCommunityToolkitSourceGeneratorsBenchmarksCsproj }} -c Release --no-build -- -a "${{ runner.temp }}" - name: Publish Benchmarks uses: actions/upload-artifact@v7 From b75ccb6334ae776337a32f7d44b39f2b4696251c Mon Sep 17 00:00:00 2001 From: Matt Goldman Date: Tue, 7 Apr 2026 07:13:16 +1000 Subject: [PATCH 4/8] Update OnValidationPropertyChanged to resolve race conditions --- .../Behaviors/Validators/ValidationBehavior.shared.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/CommunityToolkit.Maui/Behaviors/Validators/ValidationBehavior.shared.cs b/src/CommunityToolkit.Maui/Behaviors/Validators/ValidationBehavior.shared.cs index 3cb6d14433..9d1fd041b1 100644 --- a/src/CommunityToolkit.Maui/Behaviors/Validators/ValidationBehavior.shared.cs +++ b/src/CommunityToolkit.Maui/Behaviors/Validators/ValidationBehavior.shared.cs @@ -233,8 +233,15 @@ protected override async void OnViewPropertyChanged(VisualElement sender, Proper /// protected static async void OnValidationPropertyChanged(BindableObject bindable, object oldValue, object newValue) { - var validationBehavior = (ValidationBehavior)bindable; - await validationBehavior.UpdateStateAsync(validationBehavior.View, validationBehavior.Flags, false).ConfigureAwait(false); + try + { + var validationBehavior = (ValidationBehavior)bindable; + await validationBehavior.UpdateStateAsync(validationBehavior.View, validationBehavior.Flags, false); + } + catch (Exception ex) + { + Trace.WriteLine(ex); + } } static void OnIsValidPropertyChanged(BindableObject bindable, object oldValue, object newValue) From fd83e1de313a4c8a651697ee34f92e26775acdb7 Mon Sep 17 00:00:00 2001 From: Matt Goldman Date: Tue, 7 Apr 2026 07:16:27 +1000 Subject: [PATCH 5/8] Updsate LazyView LoadAsync test to allow indefinite wait for toekn expiry, and fall back to TestDuration to timeout the test. --- .../Views/LazyView/LazyViewTests.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Maui.UnitTests/Views/LazyView/LazyViewTests.cs b/src/CommunityToolkit.Maui.UnitTests/Views/LazyView/LazyViewTests.cs index fc38b93516..cfc10a72af 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Views/LazyView/LazyViewTests.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Views/LazyView/LazyViewTests.cs @@ -20,7 +20,10 @@ public async Task LoadViewAsync_CancellationTokenExpired() var lazyView = new LazyView