Skip to content
Closed
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
5 changes: 4 additions & 1 deletion src/RestSharp/ContentType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ public class ContentType : IEquatable<ContentType> {

public static readonly string[] XmlAccept = [Xml, "text/xml"];

internal static readonly SupportsContentType SupportsJson = contentType => contentType.Value.EndsWith("json", StringComparison.InvariantCultureIgnoreCase);
internal static readonly SupportsContentType SupportsXml = contentType => contentType.Value.EndsWith("xml", StringComparison.InvariantCultureIgnoreCase);

readonly string _value;

public bool Equals(ContentType? other) {
Expand All @@ -81,4 +84,4 @@ public override bool Equals(object? obj) {
}

public override int GetHashCode() => _value.GetHashCode();
}
}
4 changes: 2 additions & 2 deletions src/RestSharp/Serializers/Json/SystemTextJsonSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,5 @@ public class SystemTextJsonSerializer : IRestSerializer, ISerializer, IDeseriali
public IDeserializer Deserializer => this;
public DataFormat DataFormat => DataFormat.Json;
public string[] AcceptedContentTypes => ContentType.JsonAccept;
public SupportsContentType SupportsContentType => contentType => contentType.Value.EndsWith("json", StringComparison.InvariantCultureIgnoreCase);
}
public SupportsContentType SupportsContentType => ContentType.SupportsJson;
}
16 changes: 15 additions & 1 deletion src/RestSharp/Serializers/SerializerConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,21 @@ public SerializerConfig UseSerializer(Func<IRestSerializer> serializerFactory) {
return this;
}

public void UseDefaultSerializers() => UseSerializer<SystemTextJsonSerializer>().UseSerializer<XmlRestSerializer>();
public void UseDefaultSerializers() {
// Defer construction so custom serializers can replace defaults without loading their dependencies.
Serializers[DataFormat.Json] = new(
DataFormat.Json,
ContentType.JsonAccept,
ContentType.SupportsJson,
static () => new SystemTextJsonSerializer()
);
Serializers[DataFormat.Xml] = new(
DataFormat.Xml,
ContentType.XmlAccept,
ContentType.SupportsXml,
static () => new XmlRestSerializer()
);
}

/// <summary>
/// Replace the default serializer with a custom one
Expand Down
4 changes: 2 additions & 2 deletions src/RestSharp/Serializers/Xml/XmlRestSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public class XmlRestSerializer(IXmlSerializer serializer, IXmlDeserializer deser
public ISerializer Serializer => _serializer;
public IDeserializer Deserializer => _deserializer;
public string[] AcceptedContentTypes => ContentType.XmlAccept;
public SupportsContentType SupportsContentType => contentType => contentType.Value.EndsWith("xml", StringComparison.InvariantCultureIgnoreCase);
public SupportsContentType SupportsContentType => ContentType.SupportsXml;

public DataFormat DataFormat => DataFormat.Xml;

Expand Down Expand Up @@ -53,4 +53,4 @@ public XmlRestSerializer WithXmlDeserializer(IXmlDeserializer xmlDeserializer) {
_deserializer = xmlDeserializer;
return this;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
#if NET48
using System.Runtime.CompilerServices;
using RestSharp.Serializers;
using RestSharp.Serializers.NewtonsoftJson;

namespace RestSharp.Tests.Serializers.Json;

public class SerializerAssemblyLoadingTests {
[Fact]
public void Defaults_do_not_load_json_libraries() {
var loaded = RunIsolated(probe => probe.UseDefaults());

loaded.Should().NotContain("System.Text.Json").And.NotContain("Newtonsoft.Json");
}

[Fact]
public void Xml_only_does_not_load_json_libraries() {
var loaded = RunIsolated(probe => probe.UseXmlOnly());

loaded.Should().NotContain("System.Text.Json").And.NotContain("Newtonsoft.Json");
}

[Fact]
public void Default_json_loads_only_system_text_json() {
var loaded = RunIsolated(probe => probe.UseDefaultJson());

loaded.Should().Contain("System.Text.Json").And.NotContain("Newtonsoft.Json");
}

[Fact]
public void Newtonsoft_loads_only_newtonsoft_json() {
var loaded = RunIsolated(probe => probe.UseNewtonsoft());

loaded.Should().Contain("Newtonsoft.Json").And.NotContain("System.Text.Json");
}

[Fact]
public void Newtonsoft_only_loads_only_newtonsoft_json() {
var loaded = RunIsolated(probe => probe.UseNewtonsoftOnly());

loaded.Should().Contain("Newtonsoft.Json").And.NotContain("System.Text.Json");
}

static string[] RunIsolated(Action<IsolatedSerializerConfigurationHelper> test) {
var probeType = typeof(IsolatedSerializerConfigurationHelper);
var setup = new AppDomainSetup {
// CodeBase points to the original output directory even when xUnit shadow-copies the assembly.
ApplicationBase = Path.GetDirectoryName(new Uri(probeType.Assembly.CodeBase).LocalPath),
ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile,
LoaderOptimization = LoaderOptimization.SingleDomain
};
var domain = AppDomain.CreateDomain($"Serializer loading {Guid.NewGuid():N}", null, setup);

try {
var probe = (IsolatedSerializerConfigurationHelper)domain.CreateInstanceAndUnwrap(probeType.Assembly.FullName, probeType.FullName);
probe.GetLoadedAssemblies().Should().NotContain("System.Text.Json").And.NotContain("Newtonsoft.Json");

test(probe);

return probe.GetLoadedAssemblies();
}
finally {
AppDomain.Unload(domain);
}
}

/// <summary>
/// Exercises serializer configurations in a separate AppDomain and reports its loaded assemblies.
/// </summary>
/// <remarks>
/// Inherits from MarshalByRefObject so calls execute in the isolated domain.
/// Avoid test framework calls here, as they could load unrelated dependencies and affect the assertions.
/// </remarks>
public class IsolatedSerializerConfigurationHelper : MarshalByRefObject {
public string[] GetLoadedAssemblies() => AppDomain.CurrentDomain.GetAssemblies().Select(a => a.GetName().Name).ToArray();

public void UseDefaults() {
using var client = new RestClient();
RoundtripXml(client);
}

public void UseXmlOnly() {
using var client = new RestClient(configureSerialization: config => config.UseXml());
RoundtripXml(client);
}

public void UseDefaultJson() {
using var client = new RestClient();
RoundtripJson(client);
}

// Keep Newtonsoft references out of the default/XML path, including its JIT compilation.
[MethodImpl(MethodImplOptions.NoInlining)]
public void UseNewtonsoft() {
using var client = new RestClient(configureSerialization: config => config.UseNewtonsoftJson());
RoundtripJson(client);
RoundtripXml(client);
}

[MethodImpl(MethodImplOptions.NoInlining)]
public void UseNewtonsoftOnly() {
using var client = new RestClient(configureSerialization: config => config.UseOnlySerializer(() => new JsonNetSerializer()));
RoundtripJson(client);
}

static void RoundtripJson(RestClient client) {
var serializer = client.Serializers.GetSerializer(DataFormat.Json);
var body = serializer.Serialize(new JsonParameter(new Payload { Value = "roundtrip" }));
var result = serializer.Deserializer.Deserialize<Payload>(new RestResponse(new RestRequest()) { Content = body });
if (result?.Value != "roundtrip") throw new InvalidOperationException("JSON roundtrip failed.");
}

static void RoundtripXml(RestClient client) {
var serializer = client.Serializers.GetSerializer(DataFormat.Xml);
var body = serializer.Serialize(new XmlParameter(new Payload { Value = "roundtrip" }));
var result = serializer.Deserializer.Deserialize<Payload>(new RestResponse(new RestRequest()) { Content = body });
if (result?.Value != "roundtrip") throw new InvalidOperationException("XML roundtrip failed.");
}
}

public class Payload {
public string Value { get; set; }
}
}
#endif
17 changes: 16 additions & 1 deletion test/RestSharp.Tests/RestClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,21 @@ public void UseOnlySerializer_leaves_only_custom_serializer() {
client.Serializers.GetSerializer(DataFormat.Json).Should().NotBeNull();
}

[Theory]
[InlineData(DataFormat.Json)]
[InlineData(DataFormat.Xml)]
public void Default_registration_matches_serializer_metadata_and_creates_fresh_instances(DataFormat format) {
using var client = new RestClient();
var record = client.Serializers.Serializers[format];
var first = client.Serializers.GetSerializer(format);
var second = client.Serializers.GetSerializer(format);

record.DataFormat.Should().Be(first.DataFormat);
record.AcceptedContentTypes.Should().Equal(first.AcceptedContentTypes);
record.SupportsContentType.Should().BeSameAs(first.SupportsContentType);
second.Should().NotBeSameAs(first);
}

[Fact]
public void Should_reuse_httpClient_instance() {
using var client1 = new RestClient(new Uri("https://fake.api"), useClientFactory: true);
Expand Down Expand Up @@ -148,4 +163,4 @@ public void Should_not_set_expect_continue_on_new_http_client_default_headers()
// assert
restClient.HttpClient.DefaultRequestHeaders.ExpectContinue.Should().BeNull();
}
}
}