Skip to content

[API Proposal]: Allow configuring the equivalency behavior of BeXmlSerializable #3108

@logiclrd

Description

@logiclrd

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.Serialization namespace.
  • DataContract serialization involves classes from the System.Runtime.Serialization namespace.
  • Legacy formatter-based serialization is part of the core BCL in the System namespace.

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:

  1. 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 IEquivalencyOptions will have three new properties: ExcludeXmlIgnoredMembers, ExcludeIgnoredDataMembers and ExcludeNonSerializedFields.
  • Interface IMember will have three new properties: IsXmlIgnored, IsIgnoredDataMember and IsNonSerialized
  • Along the lines of ExcludingFields and IncludingFields, class SelfReferenceEquivalencyOptions will have three new pairs of fluent configuration methods:
    • ExcludingXmlIgnoredMembers and IncludingXmlIgnoredMembers
    • ExcludingIgnoredDataMembers and IncludingIgnoredDataMembers
    • ExcludingIgnoredDataMembers and IncludingIgnoredDataMembers
  1. The semantics of BeXmlSerializable and BeDataContractSerializable in ObjectAssertionSpecs will 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 changes

DataContract

[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 changes

BinaryFormatter (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 changes

Alternative 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-approvedAPI was approved, it can be implemented

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions