From 3a38c50be68a8c4e0c7fc834b1ec6dd6b5ec8d51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Fri, 10 Apr 2026 08:51:13 +0200 Subject: [PATCH] Restore properties after encryption to support them being reused (#713) * Restore properties after encryption to support reuse * Switch to new tuple syntax * Better name * Use existing tests instead * Add test to check correctness when sending the same object multiple times * Minimize diff revert EOF changes * Test both supported variants --------- Co-authored-by: Ramon Smits --- .../When_sending_the_same_message_twice.cs | 76 +++++++ .../When_using_Aes_with_custom.cs | 170 +++++++-------- .../When_using_Aes_with_unobtrusive_mode.cs | 195 +++++++++--------- src/MessageProperty/DecryptBehavior.cs | 4 +- src/MessageProperty/EncryptBehavior.cs | 57 +++-- src/MessageProperty/EncryptionInspector.cs | 10 +- .../ConventionBasedEncryptedStringSpecs.cs | 2 +- src/Tests/WireEncryptedStringSpecs.cs | 12 +- 8 files changed, 295 insertions(+), 231 deletions(-) create mode 100644 src/AcceptanceTests/When_sending_the_same_message_twice.cs diff --git a/src/AcceptanceTests/When_sending_the_same_message_twice.cs b/src/AcceptanceTests/When_sending_the_same_message_twice.cs new file mode 100644 index 00000000..c6075dec --- /dev/null +++ b/src/AcceptanceTests/When_sending_the_same_message_twice.cs @@ -0,0 +1,76 @@ +namespace NServiceBus.Encryption.MessageProperty.AcceptanceTests; + +using System.Collections.Generic; +using System.Threading.Tasks; +using AcceptanceTesting; +using NUnit.Framework; + +public class When_sending_the_same_message_twice : NServiceBusAcceptanceTest +{ + [Test] + public async Task Should_not_corrupt_encrypted_properties() + { + var secret = "betcha can't guess my secret"; + var messageToReuse = new MessageWithSecretData + { + Secret = secret, + EncryptedString = secret, + SubProperty = new MySecretSubProperty { Secret = secret } + }; + + var context = await Scenario.Define() + .WithEndpoint(b => b.When(async session => + { + await session.SendLocal(messageToReuse); + await session.SendLocal(messageToReuse); + })) + .Run(); + + Assert.Multiple(() => + { + foreach (var message in context.MessagesReceived) + { + Assert.That(message.Secret.Value, Is.EqualTo(secret)); + Assert.That(message.EncryptedString, Is.EqualTo(secret)); + Assert.That(message.SubProperty.Secret.Value, Is.EqualTo(secret)); + } + }); + } + + public class Context : ScenarioContext + { + public List MessagesReceived { get; } = []; + } + + public class Endpoint : EndpointConfigurationBuilder + { + public Endpoint() => EndpointSetup(config => + { + var encryptionService = new AesEncryptionService("1st", new Dictionary { { "1st", "gdDbqRpqdRbTs3mhdZh9qCaDaxJXl+e6"u8.ToArray() } }); + config.EnableMessagePropertyEncryption(encryptionService, property => property.Name.StartsWith("Encrypted") || property.PropertyType == typeof(EncryptedString)); + }); + + public class Handler(Context testContext) : IHandleMessages + { + public Task Handle(MessageWithSecretData message, IMessageHandlerContext context) + { + testContext.MessagesReceived.Add(message); + testContext.MarkAsCompleted(testContext.MessagesReceived.Count == 2); + + return Task.FromResult(0); + } + } + } + + public class MessageWithSecretData : IMessage + { + public EncryptedString Secret { get; set; } + public MySecretSubProperty SubProperty { get; set; } + public string EncryptedString { get; set; } + } + + public class MySecretSubProperty + { + public EncryptedString Secret { get; set; } + } +} \ No newline at end of file diff --git a/src/AcceptanceTests/When_using_Aes_with_custom.cs b/src/AcceptanceTests/When_using_Aes_with_custom.cs index 0a94b65a..914cf5ae 100644 --- a/src/AcceptanceTests/When_using_Aes_with_custom.cs +++ b/src/AcceptanceTests/When_using_Aes_with_custom.cs @@ -1,121 +1,105 @@ -namespace NServiceBus.Encryption.MessageProperty.AcceptanceTests +namespace NServiceBus.Encryption.MessageProperty.AcceptanceTests; + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using AcceptanceTesting; +using NUnit.Framework; + +public class When_using_Aes_with_custom : NServiceBusAcceptanceTest { - using System; - using System.Collections.Generic; - using System.Text; - using System.Threading.Tasks; - using AcceptanceTesting; - using NUnit.Framework; - - public class When_using_Aes_with_custom : NServiceBusAcceptanceTest + [Test] + public async Task Should_receive_decrypted_message() { - [Test] - public async Task Should_receive_decrypted_message() + var messageToSend = new MessageWithSecretData { - var context = await Scenario.Define() - .WithEndpoint(b => b.When(session => session.SendLocal(new MessageWithSecretData + Secret = "betcha can't guess my secret", + SubProperty = new MySecretSubProperty { Secret = "My sub secret" }, + CreditCards = + [ + new CreditCardDetails { - Secret = "betcha can't guess my secret", - SubProperty = new MySecretSubProperty - { - Secret = "My sub secret" - }, - CreditCards = - [ - new CreditCardDetails - { - ValidTo = DateTime.UtcNow.AddYears(1), - Number = "312312312312312" - }, - new CreditCardDetails - { - ValidTo = DateTime.UtcNow.AddYears(2), - Number = "543645546546456" - } - ] - }))) - .Done(c => c.GetTheMessage) - .Run(); - - Assert.Multiple(() => - { - Assert.That(context.Secret, Is.EqualTo("betcha can't guess my secret")); - Assert.That(context.SubPropertySecret, Is.EqualTo("My sub secret")); - }); + ValidTo = DateTime.UtcNow.AddYears(1), + Number = "312312312312312" + }, + new CreditCardDetails + { + ValidTo = DateTime.UtcNow.AddYears(2), + Number = "543645546546456" + } + ] + }; + + var context = await Scenario.Define() + .WithEndpoint(b => b.When(session => session.SendLocal(messageToSend))) + .Run(); + + Assert.Multiple(() => + { + Assert.That(context.Secret, Is.EqualTo(messageToSend.Secret.Value)); + Assert.That(context.SubPropertySecret, Is.EqualTo(messageToSend.SubProperty.Secret.Value)); Assert.That(context.CreditCards, Is.EquivalentTo( [ "312312312312312", "543645546546456" ])); - } - - public class Context : ScenarioContext - { - public bool GetTheMessage { get; set; } + }); + } - public string Secret { get; set; } + public class Context : ScenarioContext + { + public string Secret { get; set; } - public string SubPropertySecret { get; set; } + public string SubPropertySecret { get; set; } - public List CreditCards { get; set; } - } + public List CreditCards { get; set; } + } - public class Endpoint : EndpointConfigurationBuilder + public class Endpoint : EndpointConfigurationBuilder + { + public Endpoint() { - public Endpoint() - { - var keys = new Dictionary - { - {"1st", Encoding.ASCII.GetBytes("gdDbqRpqdRbTs3mhdZh9qCaDaxJXl+e6")} - }; + var keys = new Dictionary { { "1st", "gdDbqRpqdRbTs3mhdZh9qCaDaxJXl+e6"u8.ToArray() } }; - EndpointSetup(builder => builder.EnableMessagePropertyEncryption(new AesEncryptionService("1st", keys))); - } + EndpointSetup(builder => builder.EnableMessagePropertyEncryption(new AesEncryptionService("1st", keys))); + } - public class Handler : IHandleMessages + public class Handler(Context testContext) : IHandleMessages + { + public Task Handle(MessageWithSecretData message, IMessageHandlerContext context) { - Context testContext; - - public Handler(Context testContext) - { - this.testContext = testContext; - } + testContext.Secret = message.Secret.Value; - public Task Handle(MessageWithSecretData message, IMessageHandlerContext context) - { - testContext.Secret = message.Secret.Value; - - testContext.SubPropertySecret = message.SubProperty.Secret.Value; + testContext.SubPropertySecret = message.SubProperty.Secret.Value; - testContext.CreditCards = - [ - message.CreditCards[0].Number.Value, - message.CreditCards[1].Number.Value - ]; + testContext.CreditCards = + [ + message.CreditCards[0].Number.Value, + message.CreditCards[1].Number.Value + ]; - testContext.GetTheMessage = true; + testContext.MarkAsCompleted(); - return Task.FromResult(0); - } + return Task.FromResult(0); } } + } - public class MessageWithSecretData : IMessage - { - public EncryptedString Secret { get; set; } - public MySecretSubProperty SubProperty { get; set; } - public List CreditCards { get; set; } - } + public class MessageWithSecretData : IMessage + { + public EncryptedString Secret { get; set; } + public MySecretSubProperty SubProperty { get; set; } + public List CreditCards { get; set; } + } - public class CreditCardDetails - { - public DateTime ValidTo { get; set; } - public EncryptedString Number { get; set; } - } + public class CreditCardDetails + { + public DateTime ValidTo { get; set; } + public EncryptedString Number { get; set; } + } - public class MySecretSubProperty - { - public EncryptedString Secret { get; set; } - } + public class MySecretSubProperty + { + public EncryptedString Secret { get; set; } } } \ No newline at end of file diff --git a/src/AcceptanceTests/When_using_Aes_with_unobtrusive_mode.cs b/src/AcceptanceTests/When_using_Aes_with_unobtrusive_mode.cs index 105989d0..1b7eb3c4 100644 --- a/src/AcceptanceTests/When_using_Aes_with_unobtrusive_mode.cs +++ b/src/AcceptanceTests/When_using_Aes_with_unobtrusive_mode.cs @@ -1,133 +1,124 @@ -namespace NServiceBus.Encryption.MessageProperty.AcceptanceTests +namespace NServiceBus.Encryption.MessageProperty.AcceptanceTests; + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using AcceptanceTesting; +using AcceptanceTesting.Customization; +using NUnit.Framework; + +public class When_using_Aes_with_unobtrusive_mode : NServiceBusAcceptanceTest { - using System; - using System.Collections.Generic; - using System.Linq; - using System.Threading.Tasks; - using AcceptanceTesting; - using AcceptanceTesting.Customization; - using NUnit.Framework; - - public class When_using_Aes_with_unobtrusive_mode : NServiceBusAcceptanceTest + [Test] + public async Task Should_receive_decrypted_message() { - [Test] - public async Task Should_receive_decrypted_message() + var messageToSend = new MessageWithSecretData { - var context = await Scenario.Define() - .WithEndpoint(b => b.When(session => session.Send(new MessageWithSecretData + EncryptedSecret = "betcha can't guess my secret", + SubProperty = new MySecretSubProperty { EncryptedSecret = "My sub secret" }, + CreditCards = + [ + new CreditCardDetails { - EncryptedSecret = "betcha can't guess my secret", - SubProperty = new MySecretSubProperty - { - EncryptedSecret = "My sub secret" - }, - CreditCards = - [ - new CreditCardDetails - { - ValidTo = DateTime.UtcNow.AddYears(1), - EncryptedNumber = "312312312312312" - }, - new CreditCardDetails - { - ValidTo = DateTime.UtcNow.AddYears(2), - EncryptedNumber = "543645546546456" - } - ] - }))) - .WithEndpoint() - .Done(c => c.GetTheMessage || c.FailedMessages.Any()) - .Run(); - - Assert.Multiple(() => - { - Assert.That(context.Secret, Is.EqualTo("betcha can't guess my secret")); - Assert.That(context.SubPropertySecret, Is.EqualTo("My sub secret")); - }); + ValidTo = DateTime.UtcNow.AddYears(1), + EncryptedNumber = "312312312312312" + }, + new CreditCardDetails + { + ValidTo = DateTime.UtcNow.AddYears(2), + EncryptedNumber = "543645546546456" + } + ] + }; + + var context = await Scenario.Define() + .WithEndpoint(b => b.When(session => session.Send(messageToSend))) + .WithEndpoint() + .Run(); + + Assert.Multiple(() => + { + Assert.That(context.Secret, Is.EqualTo(messageToSend.EncryptedSecret)); + Assert.That(context.SubPropertySecret, Is.EqualTo(messageToSend.SubProperty.EncryptedSecret)); Assert.That(context.CreditCards, Is.EquivalentTo( [ "312312312312312", "543645546546456" ])); - } - - static readonly Dictionary Keys = new() - { - {"1st", "gdDbqRpqdRbTs3mhdZh9qCaDaxJXl+e6"u8.ToArray()} - }; + }); + } - public class Context : ScenarioContext - { - public bool GetTheMessage { get; set; } + static readonly Dictionary Keys = new() { { "1st", "gdDbqRpqdRbTs3mhdZh9qCaDaxJXl+e6"u8.ToArray() } }; - public string Secret { get; set; } + public class Context : ScenarioContext + { + public string Secret { get; set; } - public string SubPropertySecret { get; set; } + public string SubPropertySecret { get; set; } - public List CreditCards { get; set; } - } + public List CreditCards { get; set; } + } - public class Sender : EndpointConfigurationBuilder - { - public Sender() => - EndpointSetup(c => - { - c.Conventions().DefiningCommandsAs(t => t.Namespace != null && t.FullName == typeof(MessageWithSecretData).FullName); + public class Sender : EndpointConfigurationBuilder + { + public Sender() => + EndpointSetup(c => + { + c.Conventions().DefiningCommandsAs(t => t.Namespace != null && t.FullName == typeof(MessageWithSecretData).FullName); - c.EnableMessagePropertyEncryption(new AesEncryptionService("1st", Keys), t => t.Name.StartsWith("Encrypted")); + c.EnableMessagePropertyEncryption(new AesEncryptionService("1st", Keys), t => t.Name.StartsWith("Encrypted")); - c.ConfigureRouting() - .RouteToEndpoint(typeof(MessageWithSecretData), Conventions.EndpointNamingConvention(typeof(Receiver))); - }); - } + c.ConfigureRouting() + .RouteToEndpoint(typeof(MessageWithSecretData), Conventions.EndpointNamingConvention(typeof(Receiver))); + }); + } - public class Receiver : EndpointConfigurationBuilder - { - public Receiver() => - EndpointSetup(c => - { - c.Conventions().DefiningCommandsAs(t => t.Namespace != null && t.FullName == typeof(MessageWithSecretData).FullName); + public class Receiver : EndpointConfigurationBuilder + { + public Receiver() => + EndpointSetup(c => + { + c.Conventions().DefiningCommandsAs(t => t.Namespace != null && t.FullName == typeof(MessageWithSecretData).FullName); - c.EnableMessagePropertyEncryption(new AesEncryptionService("1st", Keys), t => t.Name.StartsWith("Encrypted")); - }); + c.EnableMessagePropertyEncryption(new AesEncryptionService("1st", Keys), t => t.Name.StartsWith("Encrypted")); + }); - public class Handler(Context testContext) : IHandleMessages + public class Handler(Context testContext) : IHandleMessages + { + public Task Handle(MessageWithSecretData message, IMessageHandlerContext context) { - public Task Handle(MessageWithSecretData message, IMessageHandlerContext context) - { - testContext.Secret = message.EncryptedSecret; + testContext.Secret = message.EncryptedSecret; - testContext.SubPropertySecret = message.SubProperty.EncryptedSecret; + testContext.SubPropertySecret = message.SubProperty.EncryptedSecret; - testContext.CreditCards = - [ - message.CreditCards[0].EncryptedNumber, - message.CreditCards[1].EncryptedNumber - ]; + testContext.CreditCards = + [ + message.CreditCards[0].EncryptedNumber, + message.CreditCards[1].EncryptedNumber + ]; - testContext.GetTheMessage = true; + testContext.MarkAsCompleted(); - return Task.CompletedTask; - } + return Task.CompletedTask; } } + } - public class MessageWithSecretData - { - public string EncryptedSecret { get; set; } - public MySecretSubProperty SubProperty { get; set; } - public List CreditCards { get; set; } - } + public class MessageWithSecretData + { + public string EncryptedSecret { get; set; } + public MySecretSubProperty SubProperty { get; set; } + public List CreditCards { get; set; } + } - public class CreditCardDetails - { - public DateTime ValidTo { get; set; } - public string EncryptedNumber { get; set; } - } + public class CreditCardDetails + { + public DateTime ValidTo { get; set; } + public string EncryptedNumber { get; set; } + } - public class MySecretSubProperty - { - public string EncryptedSecret { get; set; } - } + public class MySecretSubProperty + { + public string EncryptedSecret { get; set; } } } \ No newline at end of file diff --git a/src/MessageProperty/DecryptBehavior.cs b/src/MessageProperty/DecryptBehavior.cs index 4f851eee..56f6f5f0 100644 --- a/src/MessageProperty/DecryptBehavior.cs +++ b/src/MessageProperty/DecryptBehavior.cs @@ -17,9 +17,9 @@ public Task Invoke(IIncomingLogicalMessageContext context, Func + class EncryptBehavior(EncryptionInspector messageInspector, IEncryptionService encryptionService) : IBehavior { - public EncryptBehavior(EncryptionInspector messageInspector, IEncryptionService encryptionService) - { - this.messageInspector = messageInspector; - this.encryptionService = encryptionService; - } - - public Task Invoke(IOutgoingLogicalMessageContext context, Func next) + public async Task Invoke(IOutgoingLogicalMessageContext context, Func next) { var currentMessageToSend = context.Message.Instance; - foreach (var item in messageInspector.ScanObject(currentMessageToSend)) + var propertiesToRestore = new List<(object unencryptedValue, object target, MemberInfo member)>(); + var propertiesToEncrypt = messageInspector.ScanObject(currentMessageToSend); + + try { - EncryptMember(item.Item1, item.Item2, context); - } + foreach (var (target, member) in propertiesToEncrypt) + { + var oldValue = EncryptMember(target, member, context); + + propertiesToRestore.Add((oldValue, target, member)); + } - context.UpdateMessage(currentMessageToSend); + context.UpdateMessage(currentMessageToSend); - return next(context); + await next(context).ConfigureAwait(false); + } + finally + { + if (propertiesToEncrypt.Any()) + { + foreach (var propertyToRestore in propertiesToRestore) + { + propertyToRestore.member.SetValue(propertyToRestore.target, propertyToRestore.unencryptedValue); + } + + context.UpdateMessage(currentMessageToSend); + } + } } - void EncryptMember(object message, MemberInfo member, IOutgoingLogicalMessageContext context) + object EncryptMember(object message, MemberInfo member, IOutgoingLogicalMessageContext context) { var valueToEncrypt = member.GetValue(message); if (valueToEncrypt is EncryptedString wireEncryptedString) { + var unencryptedValue = new EncryptedString { Value = wireEncryptedString.Value }; encryptionService.EncryptValue(wireEncryptedString, context); - return; + return unencryptedValue; } if (valueToEncrypt is string stringToEncrypt) { + var unencryptedValue = stringToEncrypt; encryptionService.EncryptValue(ref stringToEncrypt, context); member.SetValue(message, stringToEncrypt); - return; + return unencryptedValue; } throw new Exception("Only string properties are supported for convention based encryption. Check the configured conventions."); } - IEncryptionService encryptionService; - EncryptionInspector messageInspector; - public class EncryptRegistration : RegisterStep { public EncryptRegistration(EncryptionInspector inspector, IEncryptionService encryptionService) - : base("MessagePropertyEncryption", typeof(EncryptBehavior), "Invokes the encryption logic", b => new EncryptBehavior(inspector, encryptionService)) - { + : base("MessagePropertyEncryption", typeof(EncryptBehavior), "Invokes the encryption logic", b => new EncryptBehavior(inspector, encryptionService)) => InsertAfter("MutateOutgoingMessages"); - } } } } \ No newline at end of file diff --git a/src/MessageProperty/EncryptionInspector.cs b/src/MessageProperty/EncryptionInspector.cs index 70937c0d..ba5ebd6b 100644 --- a/src/MessageProperty/EncryptionInspector.cs +++ b/src/MessageProperty/EncryptionInspector.cs @@ -47,7 +47,7 @@ bool IsEncryptedMember(MemberInfo arg) return false; } - public List> ScanObject(object root) + public List<(object target, MemberInfo member)> ScanObject(object root) { #pragma warning disable PS0025 // Dictionary keys should implement IEquatable - Valid use for object counting var visitedMembers = new HashSet(); @@ -56,7 +56,7 @@ public List> ScanObject(object root) } #pragma warning disable PS0025 // Dictionary keys should implement IEquatable - Valid use for object counting - List> ScanObject(object root, HashSet visitedMembers) + List<(object target, MemberInfo member)> ScanObject(object root, HashSet visitedMembers) #pragma warning restore PS0025 // Dictionary keys should implement IEquatable { if (root == null || visitedMembers.Contains(root)) @@ -68,7 +68,7 @@ List> ScanObject(object root, HashSet visitedM var members = GetFieldsAndProperties(root); - var properties = new List>(); + var properties = new List<(object target, MemberInfo member)>(); foreach (var member in members) { @@ -77,7 +77,7 @@ List> ScanObject(object root, HashSet visitedM var value = member.GetValue(root); if (value is string or EncryptedString) { - properties.Add(Tuple.Create(root, member)); + properties.Add((root, member)); continue; } throw new Exception("Only string properties are supported for convention based encryption. Check the configured conventions."); @@ -161,7 +161,7 @@ static List GetFieldsAndProperties(object target) static List NoMembers = []; - static List> AlreadyVisited = []; + static List<(object target, MemberInfo member)> AlreadyVisited = []; static ConcurrentDictionary> cache = new ConcurrentDictionary>(); } diff --git a/src/Tests/ConventionBasedEncryptedStringSpecs.cs b/src/Tests/ConventionBasedEncryptedStringSpecs.cs index 3ea979a5..f711f85f 100644 --- a/src/Tests/ConventionBasedEncryptedStringSpecs.cs +++ b/src/Tests/ConventionBasedEncryptedStringSpecs.cs @@ -19,7 +19,7 @@ public void Should_return_the_value() var result = inspector.ScanObject(message).ToList(); Assert.That(result, Has.Count.EqualTo(1)); - Assert.That(result[0].Item2.Name, Is.EqualTo("EncryptedSecret")); + Assert.That(result[0].member.Name, Is.EqualTo("EncryptedSecret")); } } diff --git a/src/Tests/WireEncryptedStringSpecs.cs b/src/Tests/WireEncryptedStringSpecs.cs index 448fcdb6..775156b6 100644 --- a/src/Tests/WireEncryptedStringSpecs.cs +++ b/src/Tests/WireEncryptedStringSpecs.cs @@ -27,7 +27,7 @@ public void Should_use_the_wireEncrypted_string() message.ListOfSecrets = [.. message.ListOfCreditCards]; var result = inspector.ScanObject(message).ToList(); - result.ForEach(x => x.Item2.SetValue(x.Item1, Create())); + result.ForEach(x => x.member.SetValue(x.target, Create())); Assert.Multiple(() => { @@ -62,7 +62,7 @@ public void Should_match_the_property_correctly() var result = inspector.ScanObject(message).ToList(); Assert.That(result, Has.Count.EqualTo(1)); - Assert.That(result[0].Item2.GetValue(result[0].Item1), Is.SameAs(message.Secret)); + Assert.That(result[0].member.GetValue(result[0].target), Is.SameAs(message.Secret)); } public class MessageWithIndexedProperties : IMessage @@ -124,7 +124,7 @@ public void Should_match_the_property_correctly() var result = inspector.ScanObject(message).ToList(); Assert.That(result, Has.Count.EqualTo(1)); - Assert.That(result[0].Item2.Name, Is.EqualTo("Secret")); + Assert.That(result[0].member.Name, Is.EqualTo("Secret")); } } @@ -143,7 +143,7 @@ public void Should_match_the_property_correctly() inspector .ScanObject(message) .ToList() - .ForEach(x => x.Item2.SetValue(x.Item1, (EncryptedString)MySecretMessage)); + .ForEach(x => x.member.SetValue(x.target, (EncryptedString)MySecretMessage)); Assert.That(message.MySecret.Value, Is.EqualTo(MySecretMessage)); } @@ -172,7 +172,7 @@ public void Should_decrypt_correctly() inspector .ScanObject(message) .ToList() - .ForEach(x => x.Item2.SetValue(x.Item1, Create())); + .ForEach(x => x.member.SetValue(x.target, Create())); Assert.That(MySecretMessage, Is.EqualTo(message.Secret.Value)); } @@ -195,7 +195,7 @@ public void Should_use_the_wireEncrypted_string() inspector .ScanObject(message) .ToList() - .ForEach(x => x.Item2.SetValue(x.Item1, Create())); + .ForEach(x => x.member.SetValue(x.target, Create())); Assert.Multiple(() => {