-
Notifications
You must be signed in to change notification settings - Fork 731
Description
Background and motivation
There are two methods in the API right now, .Should().BeXmlSerializable() and .Should().BeDataContractSerializable(). Their purpose is to determine whether that particular instance of the data type can be round-tripped through the respective serialization infrastructure. This is done by serializing it and then immediately deserializing it, and then comparing the resulting object graphs.
There are 3 serialization paradigms at play:
- XML serialization involves classes from the
System.Xml.Serializationnamespace. - DataContract serialization involves classes from the
System.Runtime.Serializationnamespace. - Legacy formatter-based serialization is part of the core BCL in the
Systemnamespace.
All three of these provide the ability to mark members as "ignored", so that their values are not stored in the resulting serialized document.
The interplay of this with the FluentAssertions API is that any instance type that has an ignored member, in which that member's value is not the default value, will always fail to pass .BeXmlSerializable() or .BeDataContractSerializable() test. The ignored field will always differ between the original subject object and the "derived" object resulting from the serialization round-trip.
I propose to correct this with two API changes:
- The ability to specify that members should be ignored based on their
[XmlIgnore]/[IgnoredDataMember]/[NonSerialized]attribution.
This will be represented in the following ways:
- Interface
IEquivalencyOptionswill have three new properties:ExcludeXmlIgnoredMembers,ExcludeIgnoredDataMembersandExcludeNonSerializedFields. - Interface
IMemberwill have three new properties:IsXmlIgnored,IsIgnoredDataMemberandIsNonSerialized - Along the lines of
ExcludingFieldsandIncludingFields, classSelfReferenceEquivalencyOptionswill have three new pairs of fluent configuration methods:ExcludingXmlIgnoredMembersandIncludingXmlIgnoredMembersExcludingIgnoredDataMembersandIncludingIgnoredDataMembersExcludingIgnoredDataMembersandIncludingIgnoredDataMembers
- The semantics of
BeXmlSerializableandBeDataContractSerializableinObjectAssertionSpecswill change to include the behaviour that ignored members do not cause the assertion to fail. The specification of the API isn't changed here, but the behaviour of the existing specification is.
API Proposal
Is this the right thing for this field?
---- Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt ----
index e4c2ff2..437089d 100644
@@ -767,7 +767,10 @@ namespace FluentAssertions.Equivalency
FluentAssertions.Equivalency.ConversionSelector ConversionSelector { get; }
FluentAssertions.Equivalency.CyclicReferenceHandling CyclicReferenceHandling { get; }
FluentAssertions.Equivalency.EnumEquivalencyHandling EnumEquivalencyHandling { get; }
+ bool ExcludeIgnoredDataMembers { get; }
bool ExcludeNonBrowsableOnExpectation { get; }
+ bool ExcludeNonSerializedFields { get; }
+ bool ExcludeXmlIgnoredMembers { get; }
bool IgnoreCase { get; }
bool IgnoreJsonPropertyCasing { get; }
bool IgnoreLeadingWhitespace { get; }
@@ -806,6 +809,9 @@ namespace FluentAssertions.Equivalency
System.Type DeclaringType { get; }
FluentAssertions.Common.CSharpAccessModifier GetterAccessibility { get; }
bool IsBrowsable { get; }
+ bool IsIgnoredDataMember { get; }
+ bool IsNonSerialized { get; }
+ bool IsXmlIgnored { get; }
System.Type ReflectedType { get; }
FluentAssertions.Common.CSharpAccessModifier SetterAccessibility { get; }
object GetValue(object obj);
@@ -935,10 +941,13 @@ namespace FluentAssertions.Equivalency
public TSelf Excluding(System.Linq.Expressions.Expression<System.Func<FluentAssertions.Equivalency.IMemberInfo, bool>> predicate) { }
public TSelf ExcludingExplicitlyImplementedProperties() { }
public TSelf ExcludingFields() { }
+ public TSelf ExcludingIgnoredDataMembers() { }
public TSelf ExcludingMembersNamed(params string[] memberNames) { }
public TSelf ExcludingMissingMembers() { }
public TSelf ExcludingNonBrowsableMembers() { }
+ public TSelf ExcludingNonSerializedFields() { }
public TSelf ExcludingProperties() { }
+ public TSelf ExcludingXmlIgnoredMembers() { }
public TSelf IgnoringCase() { }
public TSelf IgnoringCyclicReferences() { }
public TSelf IgnoringJsonPropertyCasing() { }
@@ -950,10 +959,13 @@ namespace FluentAssertions.Equivalency
public TSelf IncludingAllDeclaredProperties() { }
public TSelf IncludingAllRuntimeProperties() { }
public TSelf IncludingFields() { }
+ public TSelf IncludingIgnoredDataMembers() { }
public TSelf IncludingInternalFields() { }
public TSelf IncludingInternalProperties() { }
public TSelf IncludingNestedObjects() { }
+ public TSelf IncludingNonSerializedFields() { }
public TSelf IncludingProperties() { }
+ public TSelf IncludingXmlIgnoredMembers() { }
public TSelf PreferringDeclaredMemberTypes() { }
public TSelf PreferringRuntimeMemberTypes() { }
public TSelf ThrowingOnMissingMembers() { }
API Usage
XML
public class XmlRecord
{
public string Name { get; }
[XmlIgnore]
public int CachedValue { get; }
}
XmlRecord original = XmlGetRecord();
Record derived = XmlDeserialize(XmlSerialize(original)); // user-supplied methods
derived.Should().BeEquivalentTo(original, options => options
.ExcludingXmlIgnoredMembers());
original.Should().BeXmlSerializable(); // fails before these changesDataContract
[DataContract]
public class Record
{
[DataMember]
public string Name { get; }
[IgnoreDataMember]
public int CachedValue { get; }
}
Record original = GetRecord();
Record derived = XmlDeserialize(XmlSerialize(original)); // user-supplied methods
derived.Should().BeEquivalentTo(original, options => options
.ExcludingIgnoredDataMembers());
original.Should().BeDataContractSerializable(); // fails before these changesBinaryFormatter (and DataContract serialization)
[Serializable]
public class Record
{
// Note, these are fields. By design, BinaryFormatter only serializes fields.
public string Name;
[NonSerialized]
public int CachedValue;
}
XmlRecord original = XmlGetRecord();
Record derived = XmlDeserialize(XmlSerialize(original)); // user-supplied methods
derived.Should().BeEquivalentTo(original, options => options
.IncludingFields()
.ExcludingNonSerializedMembers());
original.Should().BeDataContractSerializable(); // fails before these changesAlternative Designs
No response
Risks
If someone is depending on .BeXmlSerializable() or .BeDataContractSerializable() in order to detect values placed in ignored members, this detection will no longer work.
Are you willing to help with a proof-of-concept (as PR in that or a separate repo) first and as pull-request later on?
Yes, please assign this issue to me.