Sitelet https://github.com/fluentassertions/fluentassertions/pull/3293/files
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 190 additions & 5 deletions Src/FluentAssertions/ObjectAssertionsExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
using System;
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;

using System.IO;
using System.Runtime.Serialization;
#if NET6_0_OR_GREATER
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
#endif
using System.Xml.Serialization;
using FluentAssertions.Common;
using FluentAssertions.Equivalency;
Expand Down Expand Up @@ -101,11 +105,193 @@ public static AndConstraint<ObjectAssertions> BeDataContractSerializable<T>(this
return new AndConstraint<ObjectAssertions>(assertions);
}

#if NET6_0_OR_GREATER
/// <summary>
/// Asserts that an object can be serialized and deserialized using <see cref="JsonSerializer"/> and that it still retains
/// the values of all serializable members.
/// </summary>
/// <param name="because">
/// A formatted phrase as is supported by <see cref="string.Format(string,object[])" /> explaining why the assertion
/// is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
/// </param>
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <paramref name="because" />.
/// </param>
public static AndConstraint<ObjectAssertions> BeJsonSerializable(this ObjectAssertions assertions,
[StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs)
=> BeJsonSerializable<object>(assertions, options => options, serializerOptions: null, because, becauseArgs);

/// <summary>
/// Asserts that an object can be serialized and deserialized using <see cref="JsonSerializer"/> and that it still retains
/// the values of all serializable members.
/// </summary>
/// <param name="options">
/// A reference to the <see cref="EquivalencyOptions{Object}"/> configuration object that can be used
/// to influence the way the object graphs are compared. You can also provide an alternative instance of the
/// <see cref="EquivalencyOptions{Object}"/> class. The global defaults are determined by the
/// <see cref="AssertionConfiguration"/> class.
/// </param>
/// <param name="because">
/// A formatted phrase as is supported by <see cref="string.Format(string,object[])" /> explaining why the assertion
/// is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
/// </param>
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <paramref name="because" />.
/// </param>
/// <exception cref="ArgumentNullException"><paramref name="options"/> is <see langword="null"/>.</exception>
public static AndConstraint<ObjectAssertions> BeJsonSerializable(this ObjectAssertions assertions,
Func<EquivalencyOptions<object>, EquivalencyOptions<object>> options,
[StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs)
=> BeJsonSerializable<object>(assertions, options, serializerOptions: null, because, becauseArgs);

/// <summary>
/// Asserts that an object can be serialized and deserialized using <see cref="JsonSerializer"/> and that it still retains
/// the values of all serializable members.
/// </summary>
/// <param name="serializerOptions">
/// The <see cref="JsonSerializerOptions"/> used for both serialization and deserialization.
/// </param>
/// <param name="because">
/// A formatted phrase as is supported by <see cref="string.Format(string,object[])" /> explaining why the assertion
/// is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
/// </param>
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <paramref name="because" />.
/// </param>
public static AndConstraint<ObjectAssertions> BeJsonSerializable(this ObjectAssertions assertions,
JsonSerializerOptions serializerOptions,
[StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs)
=> BeJsonSerializable<object>(assertions, options => options, serializerOptions, because, becauseArgs);

/// <summary>
/// Asserts that an object can be serialized and deserialized using <see cref="JsonSerializer"/> and that it still retains
/// the values of all serializable members.
/// </summary>
/// <param name="options">
/// A reference to the <see cref="EquivalencyOptions{Object}"/> configuration object that can be used
/// to influence the way the object graphs are compared. You can also provide an alternative instance of the
/// <see cref="EquivalencyOptions{Object}"/> class. The global defaults are determined by the
/// <see cref="AssertionConfiguration"/> class.
/// </param>
/// <param name="serializerOptions">
/// The <see cref="JsonSerializerOptions"/> used for both serialization and deserialization.
/// </param>
/// <param name="because">
/// A formatted phrase as is supported by <see cref="string.Format(string,object[])" /> explaining why the assertion
/// is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
/// </param>
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <paramref name="because" />.
/// </param>
/// <exception cref="ArgumentNullException"><paramref name="options"/> is <see langword="null"/>.</exception>
public static AndConstraint<ObjectAssertions> BeJsonSerializable(this ObjectAssertions assertions,
Func<EquivalencyOptions<object>, EquivalencyOptions<object>> options,
JsonSerializerOptions serializerOptions,
[StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs)
=> BeJsonSerializable<object>(assertions, options, serializerOptions, because, becauseArgs);

/// <summary>
/// Asserts that an object can be serialized and deserialized using <see cref="JsonSerializer"/> and that it still retains
/// the values of all serializable members.
/// </summary>
/// <param name="options">
/// A reference to the <see cref="EquivalencyOptions{T}"/> configuration object that can be used
/// to influence the way the object graphs are compared. You can also provide an alternative instance of the
/// <see cref="EquivalencyOptions{T}"/> class. The global defaults are determined by the
/// <see cref="AssertionConfiguration"/> class.
/// </param>
/// <param name="because">
/// A formatted phrase as is supported by <see cref="string.Format(string,object[])" /> explaining why the assertion
/// is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
/// </param>
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <paramref name="because" />.
/// </param>
/// <exception cref="ArgumentNullException"><paramref name="options"/> is <see langword="null"/>.</exception>
public static AndConstraint<ObjectAssertions> BeJsonSerializable<T>(this ObjectAssertions assertions,
Func<EquivalencyOptions<T>, EquivalencyOptions<T>> options,
[StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs)
=> BeJsonSerializable(assertions, options, serializerOptions: null, because, becauseArgs);

/// <summary>
/// Asserts that an object can be serialized and deserialized using <see cref="JsonSerializer"/> and that it still retains
/// the values of all serializable members.
/// </summary>
/// <param name="options">
/// A reference to the <see cref="EquivalencyOptions{T}"/> configuration object that can be used
/// to influence the way the object graphs are compared. You can also provide an alternative instance of the
/// <see cref="EquivalencyOptions{T}"/> class. The global defaults are determined by the
/// <see cref="AssertionConfiguration"/> class.
/// </param>
/// <param name="serializerOptions">
/// The <see cref="JsonSerializerOptions"/> used for both serialization and deserialization.
/// </param>
/// <param name="because">
/// A formatted phrase as is supported by <see cref="string.Format(string,object[])" /> explaining why the assertion
/// is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
/// </param>
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <paramref name="because" />.
/// </param>
/// <exception cref="ArgumentNullException"><paramref name="options"/> is <see langword="null"/>.</exception>
public static AndConstraint<ObjectAssertions> BeJsonSerializable<T>(this ObjectAssertions assertions,
Func<EquivalencyOptions<T>, EquivalencyOptions<T>> options,
JsonSerializerOptions serializerOptions,
[StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs)
{
Guard.ThrowIfArgumentIsNull(options);

try
{
object deserializedObject = CreateCloneUsingJsonSerializer(assertions.Subject, serializerOptions);

EquivalencyOptions<T> defaultOptions = AssertionConfiguration.Current.Equivalency.CloneDefaults<T>()
.PreferringRuntimeMemberTypes().IncludingProperties()
.Excluding(member => MemberIsIgnoredByJsonSerialization(member));

deserializedObject.Should().BeEquivalentTo((T)assertions.Subject, _ => options(defaultOptions));
}
catch (Exception exc)
{
assertions.CurrentAssertionChain
.BecauseOf(because, becauseArgs)
.FailWith("Expected {0} to be serializable{reason}, but serialization failed with:"
+ Environment.NewLine + Environment.NewLine + "{1}.",
assertions.Subject,
exc.Message);
}

return new AndConstraint<ObjectAssertions>(assertions);
}

[StackTraceHidden]
private static bool MemberIsIgnoredByJsonSerialization(IMemberInfo member)
{
Type declaringType = member.DeclaringType;
if (declaringType is null)
{
return false;
}

var memberInfo = declaringType.FindProperty(member.Name, MemberVisibility.Public | MemberVisibility.Internal)
?? (MemberInfo)declaringType.FindField(member.Name, MemberVisibility.Public | MemberVisibility.Internal);

return memberInfo?.IsDecoratedWith<JsonIgnoreAttribute>() == true;
}

[StackTraceHidden]
private static object CreateCloneUsingJsonSerializer(object subject, JsonSerializerOptions serializerOptions)
{
string json = JsonSerializer.Serialize(subject, subject.GetType(), serializerOptions);
return JsonSerializer.Deserialize(json, subject.GetType(), serializerOptions);
}
#endif

[StackTraceHidden]
private static object CreateCloneUsingDataContractSerializer(object subject)
{
using var stream = new MemoryStream();
var serializer = new DataContractSerializer(subject.GetType());
using MemoryStream stream = new();
DataContractSerializer serializer = new(subject.GetType());
serializer.WriteObject(stream, subject);
stream.Position = 0;
return serializer.ReadObject(stream);
Expand Down Expand Up @@ -203,4 +389,3 @@ private static object CreateCloneUsingXmlSerializer(object subject)
return serializer.Deserialize(stream);
}
}

Loading
Loading