diff --git a/Directory.Build.props b/Directory.Build.props
index 64361db1..0f4dddef 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -9,4 +9,8 @@
all
low
+
+
+
+
diff --git a/PropertyChanged.Fody/Config/CheckForSetterNonInterferenceConfig.cs b/PropertyChanged.Fody/Config/CheckForSetterNonInterferenceConfig.cs
new file mode 100644
index 00000000..a3625398
--- /dev/null
+++ b/PropertyChanged.Fody/Config/CheckForSetterNonInterferenceConfig.cs
@@ -0,0 +1,19 @@
+using System.Linq;
+using System.Xml;
+
+public partial class ModuleWeaver
+{
+ // default: false to preserve behavior from PropertyChanged.Fody 2.x
+ public bool EnsureNonInterferenceWithCustomSetterBehaviors = false;
+
+ public void ResolveCheckForSetterNonInterferenceConfig()
+ {
+ var value = Config?.Attributes("EnsureNonInterferenceWithCustomSetterBehaviors")
+ .Select(a => a.Value)
+ .SingleOrDefault();
+ if (value != null)
+ {
+ EnsureNonInterferenceWithCustomSetterBehaviors = XmlConvert.ToBoolean(value.ToLowerInvariant());
+ }
+ }
+}
diff --git a/PropertyChanged.sln b/PropertyChanged.sln
index eb57fc71..06aee3c9 100644
--- a/PropertyChanged.sln
+++ b/PropertyChanged.sln
@@ -68,6 +68,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PropertyChanged.Fody.Analyz
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PropertyChanged.Fody.Analyzer.Tests", "PropertyChanged.Fody.Analyzer.Tests\PropertyChanged.Fody.Analyzer.Tests.csproj", "{35A8A625-B010-4F9D-A901-9B07D9E13A16}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AssemblyWithSetterSideEffects", "TestAssemblies\AssemblyWithSetterSideEffects\AssemblyWithSetterSideEffects.csproj", "{E4E54CE2-DD54-41B9-BDC0-DF45FA575535}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -338,6 +340,18 @@ Global
{35A8A625-B010-4F9D-A901-9B07D9E13A16}.Release|ARM.Build.0 = Release|Any CPU
{35A8A625-B010-4F9D-A901-9B07D9E13A16}.Release|x86.ActiveCfg = Release|Any CPU
{35A8A625-B010-4F9D-A901-9B07D9E13A16}.Release|x86.Build.0 = Release|Any CPU
+ {E4E54CE2-DD54-41B9-BDC0-DF45FA575535}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E4E54CE2-DD54-41B9-BDC0-DF45FA575535}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E4E54CE2-DD54-41B9-BDC0-DF45FA575535}.Debug|ARM.ActiveCfg = Debug|Any CPU
+ {E4E54CE2-DD54-41B9-BDC0-DF45FA575535}.Debug|ARM.Build.0 = Debug|Any CPU
+ {E4E54CE2-DD54-41B9-BDC0-DF45FA575535}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E4E54CE2-DD54-41B9-BDC0-DF45FA575535}.Debug|x86.Build.0 = Debug|Any CPU
+ {E4E54CE2-DD54-41B9-BDC0-DF45FA575535}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E4E54CE2-DD54-41B9-BDC0-DF45FA575535}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E4E54CE2-DD54-41B9-BDC0-DF45FA575535}.Release|ARM.ActiveCfg = Release|Any CPU
+ {E4E54CE2-DD54-41B9-BDC0-DF45FA575535}.Release|ARM.Build.0 = Release|Any CPU
+ {E4E54CE2-DD54-41B9-BDC0-DF45FA575535}.Release|x86.ActiveCfg = Release|Any CPU
+ {E4E54CE2-DD54-41B9-BDC0-DF45FA575535}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -363,6 +377,7 @@ Global
{89CC72A8-43A9-4593-9FA5-61091C989B40} = {546A6338-E25E-4796-AF08-A4742E3BDF7B}
{9D3B5C35-6523-45D9-B11D-0FBB67C0866A} = {546A6338-E25E-4796-AF08-A4742E3BDF7B}
{10BDE166-0BFC-41D9-9326-805717A054B3} = {546A6338-E25E-4796-AF08-A4742E3BDF7B}
+ {E4E54CE2-DD54-41B9-BDC0-DF45FA575535} = {546A6338-E25E-4796-AF08-A4742E3BDF7B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {ABD5CA78-3690-4D62-8642-3463D9FAE144}
diff --git a/TestAssemblies/AssemblyWithSetterSideEffects/AssemblyWithSetterSideEffects.csproj b/TestAssemblies/AssemblyWithSetterSideEffects/AssemblyWithSetterSideEffects.csproj
new file mode 100644
index 00000000..9cf53932
--- /dev/null
+++ b/TestAssemblies/AssemblyWithSetterSideEffects/AssemblyWithSetterSideEffects.csproj
@@ -0,0 +1,7 @@
+
+
+
+ net48;net6.0
+ true
+
+
\ No newline at end of file
diff --git a/TestAssemblies/AssemblyWithSetterSideEffects/Classes.cs b/TestAssemblies/AssemblyWithSetterSideEffects/Classes.cs
new file mode 100644
index 00000000..cd4e736f
--- /dev/null
+++ b/TestAssemblies/AssemblyWithSetterSideEffects/Classes.cs
@@ -0,0 +1,52 @@
+using System.ComponentModel;
+
+public class WithSideEffectBeforeValueAssignment :
+ INotifyPropertyChanged
+{
+ public event PropertyChangedEventHandler PropertyChanged;
+ public void OnPropertyChanged(string propertyName)
+ {
+ PropertyChanged?.Invoke(this, new(propertyName));
+ }
+
+ string _property1;
+ public string Property1
+ {
+ get => _property1;
+ set
+ {
+ CallSideEffectBefore();
+ _property1 = value;
+ }
+ }
+
+
+ public int SideEffectBeforeCallCount { get; set; }
+ void CallSideEffectBefore() => SideEffectBeforeCallCount++;
+}
+
+public class WithSideEffectAfterValueAssignment :
+ INotifyPropertyChanged
+{
+ public event PropertyChangedEventHandler PropertyChanged;
+ public void OnPropertyChanged(string propertyName)
+ {
+ PropertyChanged?.Invoke(this, new(propertyName));
+ }
+
+
+
+ string _property1;
+ public string Property1
+ {
+ get => _property1;
+ set
+ {
+ _property1 = value;
+ CallSideEffectAfter();
+ }
+ }
+
+ public int SideEffectAfterCallCount { get; set; }
+ void CallSideEffectAfter() => SideEffectAfterCallCount++;
+}
\ No newline at end of file
diff --git a/Tests/AssemblyWithSetterSideEffectsTests.cs b/Tests/AssemblyWithSetterSideEffectsTests.cs
new file mode 100644
index 00000000..c0b9e1ea
--- /dev/null
+++ b/Tests/AssemblyWithSetterSideEffectsTests.cs
@@ -0,0 +1,99 @@
+public class AssemblyWithSetterSideEffectsTests
+{
+ static readonly string[] peVerifyIgnoreCodes =
+ {
+#if NETCOREAPP
+ "0x80131869"
+#endif
+ };
+
+ const string assemblyName = "AssemblyWithSetterSideEffects.dll";
+
+ [Theory]
+#if TEMP_REQUIRE_IMPLEMENTATION_SETTER_NON_INTERFERENCE
+ [InlineData([true, "same", "same", 2])]
+#endif
+ [InlineData([true, "different1", "different2", 2])]
+ [InlineData([false, "same", "same", 2])]
+ [InlineData([false, "different1", "different2", 2])]
+ public void CallsPreAssignmentSideEffect(bool checkForEquality, string firstAssignment, string secondAssignment, int expectedCallCount)
+ {
+ const string className = "WithSideEffectBeforeValueAssignment";
+
+ var weaver = new ModuleWeaver(){CheckForEquality = checkForEquality, EnsureNonInterferenceWithCustomSetterBehaviors = true };
+ var testResult = weaver.ExecuteTestRun(assemblyName, ignoreCodes: peVerifyIgnoreCodes);
+
+ var instance = testResult.GetInstance(className);
+
+ instance.Property1 = firstAssignment;
+ instance.Property1 = secondAssignment;
+
+ var callCount = (int)instance.SideEffectBeforeCallCount;
+ Assert.Equal(expectedCallCount, callCount);
+ }
+
+ [Theory]
+#if TEMP_REQUIRE_IMPLEMENTATION_SETTER_NON_INTERFERENCE
+ [InlineData([true, "same", "same", 2])]
+#endif
+ [InlineData([true, "different1", "different2", 2])]
+ [InlineData([false, "same", "same", 2])]
+ [InlineData([false, "different1", "different2", 2])]
+ public void CallsPostAssignmentSideEffect(bool checkForEquality, string firstAssignment, string secondAssignment, int expectedCallCount)
+ {
+ const string className = "WithSideEffectAfterValueAssignment";
+
+ var weaver = new ModuleWeaver() { CheckForEquality = checkForEquality, EnsureNonInterferenceWithCustomSetterBehaviors = true };
+ var testResult = weaver.ExecuteTestRun(assemblyName, ignoreCodes: peVerifyIgnoreCodes);
+
+ var instance = testResult.GetInstance(className);
+
+ instance.Property1 = firstAssignment;
+ instance.Property1 = secondAssignment;
+
+ var callCount = (int)instance.SideEffectAfterCallCount;
+ Assert.Equal(expectedCallCount, callCount);
+ }
+
+ [Theory]
+ [InlineData([true, "same", "same", 1])]
+ [InlineData([true, "different1", "different2", 2])]
+ [InlineData([false, "same", "same", 2])]
+ [InlineData([false, "different1", "different2", 2])]
+ public void CallsPreAssignmentSideEffectLegacy(bool checkForEquality, string firstAssignment, string secondAssignment, int expectedCallCount)
+ {
+ const string className = "WithSideEffectBeforeValueAssignment";
+
+ var weaver = new ModuleWeaver() { CheckForEquality = checkForEquality, EnsureNonInterferenceWithCustomSetterBehaviors = false };
+ var testResult = weaver.ExecuteTestRun(assemblyName, ignoreCodes: peVerifyIgnoreCodes);
+
+ var instance = testResult.GetInstance(className);
+
+ instance.Property1 = firstAssignment;
+ instance.Property1 = secondAssignment;
+
+ var callCount = (int)instance.SideEffectBeforeCallCount;
+ Assert.Equal(expectedCallCount, callCount);
+ }
+
+ [Theory]
+ [InlineData([true, "same", "same", 1])]
+ [InlineData([true, "different1", "different2", 2])]
+ [InlineData([false, "same", "same", 2])]
+ [InlineData([false, "different1", "different2", 2])]
+ public void CallsPostAssignmentSideEffectLegacy(bool checkForEquality, string firstAssignment, string secondAssignment, int expectedCallCount)
+ {
+ const string className = "WithSideEffectAfterValueAssignment";
+
+ var weaver = new ModuleWeaver() { CheckForEquality = checkForEquality, EnsureNonInterferenceWithCustomSetterBehaviors = false };
+ var testResult = weaver.ExecuteTestRun(assemblyName, ignoreCodes: peVerifyIgnoreCodes);
+
+ var instance = testResult.GetInstance(className);
+
+ instance.Property1 = firstAssignment;
+ instance.Property1 = secondAssignment;
+
+ var callCount = (int)instance.SideEffectAfterCallCount;
+ Assert.Equal(expectedCallCount, callCount);
+ }
+}
\ No newline at end of file
diff --git a/Tests/CheckForSetterNonInterferenceConfigTests.cs b/Tests/CheckForSetterNonInterferenceConfigTests.cs
new file mode 100644
index 00000000..2f491621
--- /dev/null
+++ b/Tests/CheckForSetterNonInterferenceConfigTests.cs
@@ -0,0 +1,48 @@
+using System.Xml.Linq;
+
+public class CheckForSetterNonInterferenceConfigTests
+{
+ [Fact]
+ public void False()
+ {
+ var xElement = XElement.Parse("");
+ var weaver = new ModuleWeaver { Config = xElement };
+ weaver.ResolveCheckForSetterNonInterferenceConfig();
+ Assert.False(weaver.EnsureNonInterferenceWithCustomSetterBehaviors);
+ }
+
+ [Fact]
+ public void False0()
+ {
+ var xElement = XElement.Parse("");
+ var weaver = new ModuleWeaver { Config = xElement };
+ weaver.ResolveCheckForSetterNonInterferenceConfig();
+ Assert.False(weaver.EnsureNonInterferenceWithCustomSetterBehaviors);
+ }
+
+ [Fact]
+ public void True()
+ {
+ var xElement = XElement.Parse("");
+ var weaver = new ModuleWeaver { Config = xElement };
+ weaver.ResolveCheckForSetterNonInterferenceConfig();
+ Assert.True(weaver.EnsureNonInterferenceWithCustomSetterBehaviors);
+ }
+
+ [Fact]
+ public void True1()
+ {
+ var xElement = XElement.Parse("");
+ var weaver = new ModuleWeaver { Config = xElement };
+ weaver.ResolveCheckForSetterNonInterferenceConfig();
+ Assert.True(weaver.EnsureNonInterferenceWithCustomSetterBehaviors);
+ }
+
+ [Fact]
+ public void Default()
+ {
+ var weaver = new ModuleWeaver();
+ weaver.ResolveCheckForSetterNonInterferenceConfig();
+ Assert.False(weaver.EnsureNonInterferenceWithCustomSetterBehaviors);
+ }
+}
\ No newline at end of file
diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj
index 4f3ebde1..e0b0db7f 100644
--- a/Tests/Tests.csproj
+++ b/Tests/Tests.csproj
@@ -36,5 +36,6 @@
+