diff --git a/docs/members-throw.md b/docs/members-throw.md index e064f8a8b..23bb5979d 100644 --- a/docs/members-throw.md +++ b/docs/members-throw.md @@ -35,7 +35,7 @@ public Task CustomExceptionPropFluent() .IgnoreMembersThatThrow(); } ``` -snippet source | anchor +snippet source | anchor Or globally: @@ -45,7 +45,7 @@ Or globally: ```cs VerifierSettings.IgnoreMembersThatThrow(); ``` -snippet source | anchor +snippet source | anchor Result: diff --git a/docs/obsolete-members.md b/docs/obsolete-members.md index 7d2592bc3..f6caf6e61 100644 --- a/docs/obsolete-members.md +++ b/docs/obsolete-members.md @@ -31,7 +31,7 @@ public Task WithObsoleteProp() return Verify(target); } ``` -snippet source | anchor +snippet source | anchor Result: @@ -79,7 +79,7 @@ public Task WithObsoletePropIncludedFluent() .IncludeObsoletes(); } ``` -snippet source | anchor +snippet source | anchor Or globally: @@ -89,7 +89,7 @@ Or globally: ```cs VerifierSettings.IncludeObsoletes(); ``` -snippet source | anchor +snippet source | anchor Result: diff --git a/docs/serializer-settings.md b/docs/serializer-settings.md index e69de29bb..a6c3f77b1 100644 --- a/docs/serializer-settings.md +++ b/docs/serializer-settings.md @@ -0,0 +1,1279 @@ + + +# Serializer settings + +Verify uses [Argon](https://github.com/SimonCropp/Argon) for serialization. See [Default Settings](#default-settings) for on how Argon is used and instructions on how to control that usage. + +Serialization settings can be customized at three levels: + + * Method: Will run the verification in the current test method. + * Class: Will run for all verifications in all test methods for a test class. + * Global: Will run for test methods on all tests. + + +## Not valid json + +Note that the output is technically not valid json. + + * Names and values are not quoted. + * Newlines are not escaped. + +The reason for these is that it makes approval files cleaner and easier to read and visualize/understand differences. + + +### UseStrictJson + +To use strict json call `VerifierSettings.UseStrictJson`: + + +#### Globally + + + +```cs +[ModuleInitializer] +public static void Init() => + VerifierSettings.UseStrictJson(); +``` +snippet source | anchor + + + +#### Instance + + + +```cs +var target = new TheTarget +{ + Value = "Foo" +}; +var settings = new VerifySettings(); +settings.UseStrictJson(); +await Verify(target, settings); +``` +snippet source | anchor + + + +#### Fluent + + + +```cs +var target = new TheTarget +{ + Value = "Foo" +}; +await Verify(target) + .UseStrictJson(); +``` +snippet source | anchor + + + +#### Result + +Then this results in + + * The default `.received.` and `.verified.` extensions for serialized verification to be `.json`. + * `JsonTextWriter.QuoteChar` to be `"`. + * `JsonTextWriter.QuoteName` to be `true`. + +Then when an object is verified: + + + +```cs +var target = new TheTarget +{ + Value = "Foo" +}; +await Verify(target); +``` +snippet source | anchor + + +The resulting file will be: + + + +```json +{ + "Value": "Foo" +} +``` +snippet source | anchor + + + +## Default settings + +Verify uses [Argon](https://github.com/SimonCropp/Argon) for serialization. + +> Argon is a JSON framework for .NET. It is a hard fork of Newtonsoft.Json. + +See [Argon documentation](https://github.com/SimonCropp/Argon/blob/main/docs/readme.md) + +The default `JsonSerializerSettings` are: + + + +```cs +var settings = new JsonSerializerSettings +{ + Formatting = Formatting.Indented, + ReferenceLoopHandling = ReferenceLoopHandling.Ignore, + DefaultValueHandling = DefaultValueHandling.Ignore +}; +``` +snippet source | anchor + + + +### Modify Defaults + + +#### Globally + + + +```cs +VerifierSettings + .AddExtraSettings(_ => + _.TypeNameHandling = TypeNameHandling.All); +``` +snippet source | anchor + + + +#### Instance + + + +```cs +[Fact] +public Task AddExtraSettings() +{ + var settings = new VerifySettings(); + settings + .AddExtraSettings( + _ => _.SerializeError = (currentObject, originalObject, location, member, exception, handled) => + Console.WriteLine(member)); + return Verify("Value", settings); +} +``` +snippet source | anchor + + + +#### Fluent + + + +```cs +[Fact] +public Task AddExtraSettingsFluent() => + Verify("Value") + .AddExtraSettings( + _ => _.SerializeError = (currentObject, originalObject, location, member, exception, handled) => + Console.WriteLine(member)); +``` +snippet source | anchor + + + +## QuoteName is false + +[JsonTextWriter.QuoteName](https://www.newtonsoft.com/json/help/html/P_Newtonsoft_Json_JsonTextWriter_QuoteName.htm) is set to false. The reason for this is that it makes approval files cleaner and easier to read and visualize/understand differences. + + +## Empty collections are ignored + +By default empty collections are ignored during verification. + +To disable this behavior globally use: + + + +```cs +VerifierSettings.DontIgnoreEmptyCollections(); +``` +snippet source | anchor + + + +## Changing Json.NET settings + +Extra Json.NET settings can be made: + + +### Globally + + + +```cs +VerifierSettings.AddExtraSettings( + _ => _.TypeNameHandling = TypeNameHandling.All); +``` +snippet source | anchor + + + +### Instance + + + +```cs +var settings = new VerifySettings(); +settings.AddExtraSettings( + _ => _.TypeNameHandling = TypeNameHandling.All); +``` +snippet source | anchor + + + +### Json.NET Converter + +One common use case is to register a custom [JsonConverter](https://www.newtonsoft.com/json/help/html/CustomJsonConverter.htm). As only writing is required, to help with this there is `WriteOnlyJsonConverter`, and `WriteOnlyJsonConverter`. + + + +```cs +class CompanyConverter : + WriteOnlyJsonConverter +{ + public override void Write(VerifyJsonWriter writer, Company company) => + writer.WriteMember(company, company.Name, "Name"); +} +``` +snippet source | anchor + + + + +```cs +VerifierSettings.AddExtraSettings( + _ => _.Converters.Add(new CompanyConverter())); +``` +snippet source | anchor + + + +#### VerifyJsonWriter + +`VerifyJsonWriter` exposes the following members: + + * `Counter` property that gives programmatic access to the counting behavior used by [Guid](guids.md), [Date](dates.md), and [Id](#numeric-ids-are-scrubbed) scrubbing. + * `Serializer` property that exposes the current `JsonSerializer`. + * `Serialize(object value)` is a convenience method that calls `JsonSerializer.Serialize` passing in the writer instance and the `value` parameter. + * `WriteProperty(T target, TMember value, string name)` method that writes a property name and value while respecting other custom serialization settings eg [member converters](#converting-a-member), [ignore rules](#ignoring-a-type) etc. + + +## Scoped settings + + + +```cs +[Fact] +public Task ScopedSerializer() +{ + var person = new Person + { + GivenNames = "John", + FamilyName = "Smith" + }; + var settings = new VerifySettings(); + settings.AddExtraSettings(_ => _.TypeNameHandling = TypeNameHandling.All); + return Verify(person, settings); +} + +[Fact] +public Task ScopedSerializerFluent() +{ + var person = new Person + { + GivenNames = "John", + FamilyName = "Smith" + }; + return Verify(person) + .AddExtraSettings(_ => _.TypeNameHandling = TypeNameHandling.All); +} +``` +snippet source | anchor + + +Result: + + + +```txt +{ + $type: VerifyObjectSamples.Person, + GivenNames: John, + FamilyName: Smith +} +``` +snippet source | anchor + + + +## Ignoring a type + +To ignore all members that match a certain type: + + + +```cs +[Fact] +public Task IgnoreType() +{ + var target = new IgnoreTypeTarget + { + ToIgnore = new() + { + Property = "Value" + }, + ToIgnoreNullable = new() + { + Property = "Value" + }, + ToIgnoreByInterface = new() + { + Property = "Value" + }, + ToIgnoreByBase = new() + { + Property = "Value" + }, + ToIgnoreByBaseGeneric = new() + { + Property = "Value" + }, + ToIgnoreByType = new() + { + Property = "Value" + }, + ToInclude = new() + { + Property = "Value" + }, + ToIncludeNullable = new() + { + Property = "Value" + }, + ToIgnoreStruct = new("Value"), + ToIgnoreStructNullable = new("Value"), + ToIncludeStruct = new("Value"), + ToIncludeStructNullable = new("Value") + }; + var settings = new VerifySettings(); + settings.IgnoreMembersWithType(); + settings.IgnoreMembersWithType(); + settings.IgnoreMembersWithType(); + settings.IgnoreMembersWithType(); + settings.IgnoreMembersWithType(typeof(BaseToIgnoreGeneric<>)); + settings.IgnoreMembersWithType(); + return Verify(target, settings); +} + +[Fact] +public Task IgnoreTypeFluent() +{ + var target = new IgnoreTypeTarget + { + ToIgnore = new() + { + Property = "Value" + }, + ToIgnoreNullable = new() + { + Property = "Value" + }, + ToIgnoreByInterface = new() + { + Property = "Value" + }, + ToIgnoreByBase = new() + { + Property = "Value" + }, + ToIgnoreByBaseGeneric = new() + { + Property = "Value" + }, + ToIgnoreByType = new() + { + Property = "Value" + }, + ToInclude = new() + { + Property = "Value" + }, + ToIncludeNullable = new() + { + Property = "Value" + }, + ToIgnoreStruct = new("Value"), + ToIgnoreStructNullable = new("Value"), + ToIncludeStruct = new("Value"), + ToIncludeStructNullable = new("Value") + }; + return Verify(target) + .IgnoreMembersWithType() + .IgnoreMembersWithType() + .IgnoreMembersWithType() + .IgnoreMembersWithType() + .IgnoreMembersWithType(typeof(BaseToIgnoreGeneric<>)) + .IgnoreMembersWithType(); +} +``` +snippet source | anchor + + +Or globally: + + + +```cs +VerifierSettings.IgnoreMembersWithType(); +``` +snippet source | anchor + + +Result: + + + +```txt +{ + ToInclude: { + Property: Value + }, + ToIncludeNullable: { + Property: Value + }, + ToIncludeStruct: { + Property: Value + }, + ToIncludeStructNullable: { + Property: Value + } +} +``` +snippet source | anchor + + + +## Scrub a type + +To scrub all members that match a certain type: + + + +```cs +[Fact] +public Task ScrubType() +{ + var target = new IgnoreTypeTarget + { + ToIgnore = new() + { + Property = "Value" + }, + ToIgnoreNullable = new() + { + Property = "Value" + }, + ToIgnoreByInterface = new() + { + Property = "Value" + }, + ToIgnoreByBase = new() + { + Property = "Value" + }, + ToIgnoreByBaseGeneric = new() + { + Property = "Value" + }, + ToIgnoreByType = new() + { + Property = "Value" + }, + ToInclude = new() + { + Property = "Value" + }, + ToIncludeNullable = new() + { + Property = "Value" + }, + ToIgnoreStruct = new("Value"), + ToIgnoreStructNullable = new("Value"), + ToIncludeStruct = new("Value"), + ToIncludeStructNullable = new("Value") + }; + var settings = new VerifySettings(); + settings.ScrubMembersWithType(); + settings.ScrubMembersWithType(); + settings.ScrubMembersWithType(); + settings.ScrubMembersWithType(); + settings.ScrubMembersWithType(typeof(BaseToIgnoreGeneric<>)); + settings.ScrubMembersWithType(); + return Verify(target, settings); +} + +[Fact] +public Task ScrubTypeFluent() +{ + var target = new IgnoreTypeTarget + { + ToIgnore = new() + { + Property = "Value" + }, + ToIgnoreNullable = new() + { + Property = "Value" + }, + ToIgnoreByInterface = new() + { + Property = "Value" + }, + ToIgnoreByBase = new() + { + Property = "Value" + }, + ToIgnoreByBaseGeneric = new() + { + Property = "Value" + }, + ToIgnoreByType = new() + { + Property = "Value" + }, + ToInclude = new() + { + Property = "Value" + }, + ToIncludeNullable = new() + { + Property = "Value" + }, + ToIgnoreStruct = new("Value"), + ToIgnoreStructNullable = new("Value"), + ToIncludeStruct = new("Value"), + ToIncludeStructNullable = new("Value") + }; + return Verify(target) + .ScrubMembersWithType() + .ScrubMembersWithType() + .ScrubMembersWithType() + .ScrubMembersWithType() + .ScrubMembersWithType(typeof(BaseToIgnoreGeneric<>)) + .ScrubMembersWithType(); +} +``` +snippet source | anchor + + +Or globally: + + + +```cs +VerifierSettings.ScrubMembersWithType(); +``` +snippet source | anchor + + +Result: + + + +```txt +{ + ToIgnore: {Scrubbed}, + ToIgnoreByType: {Scrubbed}, + ToIgnoreByInterface: {Scrubbed}, + ToIgnoreByBase: {Scrubbed}, + ToIgnoreByBaseGeneric: {Scrubbed}, + ToIgnoreNullable: {Scrubbed}, + ToIgnoreStruct: {Scrubbed}, + ToIgnoreStructNullable: {Scrubbed}, + ToInclude: { + Property: Value + }, + ToIncludeNullable: { + Property: Value + }, + ToIncludeStruct: { + Property: Value + }, + ToIncludeStructNullable: { + Property: Value + } +} +``` +snippet source | anchor + + + +## Ignoring an instance + +To ignore instances of a type based on delegate: + + + +```cs +[Fact] +public Task AddIgnoreInstance() +{ + var target = new IgnoreInstanceTarget + { + ToIgnore = new() + { + Property = "Ignore" + }, + ToInclude = new() + { + Property = "Include" + } + }; + var settings = new VerifySettings(); + settings.IgnoreInstance(_ => _.Property == "Ignore"); + return Verify(target, settings); +} + +[Fact] +public Task AddIgnoreInstanceFluent() +{ + var target = new IgnoreInstanceTarget + { + ToIgnore = new() + { + Property = "Ignore" + }, + ToInclude = new() + { + Property = "Include" + } + }; + return Verify(target) + .IgnoreInstance(_ => _.Property == "Ignore"); +} +``` +snippet source | anchor + + +Or globally: + + + +```cs +VerifierSettings.IgnoreInstance(_ => _.Property == "Ignore"); +``` +snippet source | anchor + + +Result: + + + +```txt +{ + ToInclude: { + Property: Include + } +} +``` +snippet source | anchor + + + +## Scrub a instance + +To scrub instances of a type based on delegate: + + + +```cs +[Fact] +public Task AddScrubInstance() +{ + var target = new IgnoreInstanceTarget + { + ToIgnore = new() + { + Property = "Ignore" + }, + ToInclude = new() + { + Property = "Include" + } + }; + var settings = new VerifySettings(); + settings.ScrubInstance(_ => _.Property == "Ignore"); + return Verify(target, settings); +} + +[Fact] +public Task AddScrubInstanceFluent() +{ + var target = new IgnoreInstanceTarget + { + ToIgnore = new() + { + Property = "Ignore" + }, + ToInclude = new() + { + Property = "Include" + } + }; + return Verify(target) + .ScrubInstance(_ => _.Property == "Ignore"); +} +``` +snippet source | anchor + + +Or globally: + + + +```cs +VerifierSettings.ScrubInstance(_ => _.Property == "Ignore"); +``` +snippet source | anchor + + +Result: + + + +```txt +{ + ToIgnore: {Scrubbed}, + ToInclude: { + Property: Include + } +} +``` +snippet source | anchor + + + +## Ignore member by expressions + +To ignore members of a certain type using an expression: + + + +```cs +[Fact] +public Task IgnoreMemberByExpression() +{ + var target = new IgnoreExplicitTarget + { + Include = "Value", + Field = "Value", + Property = "Value", + PropertyWithPropertyName = "Value" + }; + var settings = new VerifySettings(); + settings.IgnoreMembers( + _ => _.Property, + _ => _.PropertyWithPropertyName, + _ => _.Field, + _ => _.GetOnlyProperty, + _ => _.PropertyThatThrows); + return Verify(target, settings); +} + +[Fact] +public Task IgnoreMemberByExpressionFluent() +{ + var target = new IgnoreExplicitTarget + { + Include = "Value", + Field = "Value", + Property = "Value" + }; + return Verify(target) + .IgnoreMembers( + _ => _.Property, + _ => _.Field, + _ => _.GetOnlyProperty, + _ => _.PropertyThatThrows); +} +``` +snippet source | anchor + + +Or globally + + + +```cs +VerifierSettings.IgnoreMembers( + _ => _.Property, + _ => _.PropertyWithPropertyName, + _ => _.Field, + _ => _.GetOnlyProperty, + _ => _.PropertyThatThrows); +``` +snippet source | anchor + + +Result: + + + +```txt +{ + Include: Value +} +``` +snippet source | anchor + + + +## Scrub member by expressions + +To scrub members of a certain type using an expression: + + + +```cs +[Fact] +public Task ScrubMemberByExpression() +{ + var target = new IgnoreExplicitTarget + { + Include = "Value", + Field = "Value", + Property = "Value", + PropertyWithPropertyName = "Value" + }; + var settings = new VerifySettings(); + settings.ScrubMembers( + _ => _.Property, + _ => _.PropertyWithPropertyName, + _ => _.Field, + _ => _.GetOnlyProperty, + _ => _.PropertyThatThrows); + return Verify(target, settings); +} + +[Fact] +public Task ScrubMemberByExpressionFluent() +{ + var target = new IgnoreExplicitTarget + { + Include = "Value", + Field = "Value", + Property = "Value" + }; + return Verify(target) + .ScrubMembers( + _ => _.Property, + _ => _.Field, + _ => _.GetOnlyProperty, + _ => _.PropertyThatThrows); +} +``` +snippet source | anchor + + +Or globally + + + +```cs +VerifierSettings.ScrubMembers( + _ => _.Property, + _ => _.PropertyWithPropertyName, + _ => _.Field, + _ => _.GetOnlyProperty, + _ => _.PropertyThatThrows); +``` +snippet source | anchor + + +Result: + + + +```txt +{ + Include: Value, + Field: {Scrubbed}, + Property: {Scrubbed}, + _Custom: {Scrubbed}, + GetOnlyProperty: {Scrubbed}, + PropertyThatThrows: {Scrubbed} +} +``` +snippet source | anchor + + + +## Ignore member by name + +To ignore members of a certain type using type and name: + + + +```cs +[Fact] +public Task IgnoreMemberByName() +{ + var target = new IgnoreExplicitTarget + { + Include = "Value", + Field = "Value", + Property = "Value", + PropertyByName = "Value" + }; + var settings = new VerifySettings(); + + // For all types + settings.IgnoreMember("PropertyByName"); + + // For a specific type + settings.IgnoreMember(typeof(IgnoreExplicitTarget), "Property"); + + // For a specific type generic + settings.IgnoreMember("Field"); + + // For a specific type with expression + settings.IgnoreMember(_ => _.PropertyThatThrows); + + return Verify(target, settings); +} + +[Fact] +public Task IgnoreMemberByNameFluent() +{ + var target = new IgnoreExplicitTarget + { + Include = "Value", + Field = "Value", + Property = "Value", + PropertyByName = "Value" + }; + return Verify(target) + // For all types + .IgnoreMember("PropertyByName") + + // For a specific type + .IgnoreMember(typeof(IgnoreExplicitTarget), "Property") + + // For a specific type generic + .IgnoreMember("Field") + + // For a specific type with expression + .IgnoreMember(_ => _.PropertyThatThrows); +} +``` +snippet source | anchor + + +Or globally: + + + +```cs +// For all types +VerifierSettings.IgnoreMember("PropertyByName"); + +// For a specific type +VerifierSettings.IgnoreMember(typeof(IgnoreExplicitTarget), "Property"); + +// For a specific type generic +VerifierSettings.IgnoreMember("Field"); + +// For a specific type with expression +VerifierSettings.IgnoreMember(_ => _.PropertyThatThrows); +``` +snippet source | anchor + + +Result: + + + +```txt +{ + Include: Value, + GetOnlyProperty: asd +} +``` +snippet source | anchor + + + +## Scrub member by name + +To scrub members of a certain type using type and name: + + + +```cs +[Fact] +public Task ScrubMemberByName() +{ + var target = new IgnoreExplicitTarget + { + Include = "Value", + Field = "Value", + Property = "Value", + PropertyByName = "Value" + }; + var settings = new VerifySettings(); + + // For all types + settings.ScrubMember("PropertyByName"); + + // For a specific type + settings.ScrubMember(typeof(IgnoreExplicitTarget), "Property"); + + // For a specific type generic + settings.ScrubMember("Field"); + + // For a specific type with expression + settings.ScrubMember(_ => _.PropertyThatThrows); + + return Verify(target, settings); +} + +[Fact] +public Task ScrubMemberByNameFluent() +{ + var target = new IgnoreExplicitTarget + { + Include = "Value", + Field = "Value", + Property = "Value", + PropertyByName = "Value" + }; + return Verify(target) + // For all types + .ScrubMember("PropertyByName") + + // For a specific type + .ScrubMember(typeof(IgnoreExplicitTarget), "Property") + + // For a specific type generic + .ScrubMember("Field") + + // For a specific type with expression + .ScrubMember(_ => _.PropertyThatThrows); +} +``` +snippet source | anchor + + +Or globally: + + + +```cs +// For all types +VerifierSettings.ScrubMember("PropertyByName"); + +// For a specific type +VerifierSettings.ScrubMember(typeof(IgnoreExplicitTarget), "Property"); + +// For a specific type generic +VerifierSettings.ScrubMember("Field"); + +// For a specific type with expression +VerifierSettings.ScrubMember(_ => _.PropertyThatThrows); +``` +snippet source | anchor + + +Result: + + + +```txt +{ + Include: Value, + Field: {Scrubbed}, + Property: {Scrubbed}, + PropertyByName: {Scrubbed}, + GetOnlyProperty: asd, + PropertyThatThrows: {Scrubbed} +} +``` +snippet source | anchor + + + +## TreatAsString + +Certain types, when passed directly in to Verify, are written directly without going through json serialization. + +The default mapping is: + + + +```cs +{ + typeof(StringBuilder), (target, _) => ((StringBuilder) target).ToString() +}, +{ + typeof(StringWriter), (target, _) => ((StringWriter) target).ToString() +}, +{ + typeof(bool), (target, _) => ((bool) target).ToString(Culture.InvariantCulture) +}, +{ + typeof(short), (target, _) => ((short) target).ToString(Culture.InvariantCulture) +}, +{ + typeof(ushort), (target, _) => ((ushort) target).ToString(Culture.InvariantCulture) +}, +{ + typeof(int), (target, _) => ((int) target).ToString(Culture.InvariantCulture) +}, +{ + typeof(uint), (target, _) => ((uint) target).ToString(Culture.InvariantCulture) +}, +{ + typeof(long), (target, _) => ((long) target).ToString(Culture.InvariantCulture) +}, +{ + typeof(ulong), (target, _) => ((ulong) target).ToString(Culture.InvariantCulture) +}, +{ + typeof(decimal), (target, _) => ((decimal) target).ToString(Culture.InvariantCulture) +}, +{ + typeof(BigInteger), (target, _) => ((BigInteger) target).ToString(Culture.InvariantCulture) +}, +#if NET5_0_OR_GREATER +{ + typeof(Half), (target, _) => ((Half) target).ToString(Culture.InvariantCulture) +}, +#endif +#if NET6_0_OR_GREATER +{ + typeof(Date), (target, _) => + { + var date = (Date) target; + return date.ToString("yyyy-MM-dd", Culture.InvariantCulture); + } +}, +{ + typeof(Time), (target, _) => + { + var time = (Time) target; + return time.ToString("h:mm tt", Culture.InvariantCulture); + } +}, +#endif +{ + typeof(float), (target, _) => ((float) target).ToString(Culture.InvariantCulture) +}, +{ + typeof(double), (target, _) => ((double) target).ToString(Culture.InvariantCulture) +}, +{ + typeof(Guid), (target, _) => ((Guid) target).ToString() +}, +{ + typeof(DateTime), (target, _) => DateFormatter.ToJsonString((DateTime) target) +}, +{ + typeof(DateTimeOffset), (target, _) => DateFormatter.ToJsonString((DateTimeOffset) target) +}, +{ + typeof(XmlNode), (target, _) => + { + var converted = (XmlNode) target; + var document = XDocument.Parse(converted.OuterXml); + return new(document.ToString(), "xml"); + } +}, +{ + typeof(XElement), (target, settings) => + { + var converted = (XElement) target; + return new(converted.ToString(), "xml"); + } +}, +``` +snippet source | anchor + + +This bypasses the Guid and DateTime scrubbing. + +Extra types can be added to this mapping: + + + +```cs +VerifierSettings.TreatAsString( + (target, settings) => target.Property); +``` +snippet source | anchor + + + +## Converting a member + +The value of a member can be mutated before serialization: + + + +```cs +[ModuleInitializer] +public static void MemberConverterByExpressionInit() +{ + // using only the member + VerifierSettings.MemberConverter( + expression: _ => _.Field, + converter: member => $"{member}_Suffix"); + + // using target and member + VerifierSettings.MemberConverter( + expression: _ => _.Property, + converter: (target, member) => $"{target}_{member}_Suffix"); +} + +[Fact] +public Task MemberConverterByExpression() +{ + var input = new MemberTarget + { + Field = "FieldValue", + Property = "PropertyValue" + }; + + return Verify(input); +} +``` +snippet source | anchor + + + +## See also + + * [Obsolete members](/docs/obsolete-members.md) + * [Guids](/docs/guids.md) + * [Dates](/docs/dates.md) + * [Scrubbing](/docs/scrubbers.md) + * [Members that throw](/docs/members-throw.md) + * [Ordering](/docs/ordering.md) + * [Encoding](/docs/encoding.md) + * [JsonAppender](/docs/jsonappender.md)