Skip to content

Commit ce07172

Browse files
committed
Fix crash on atomic:operations requests when trace logging is turned on
1 parent 24b9546 commit ce07172

File tree

11 files changed

+520
-92
lines changed

11 files changed

+520
-92
lines changed

src/JsonApiDotNetCore/Middleware/TraceLogWriter.cs

+73-32
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
using System.Text.Encodings.Web;
55
using System.Text.Json;
66
using System.Text.Json.Serialization;
7+
using JsonApiDotNetCore.Configuration;
8+
using JsonApiDotNetCore.Resources;
9+
using JsonApiDotNetCore.Resources.Annotations;
710
using Microsoft.Extensions.Logging;
811

912
namespace JsonApiDotNetCore.Middleware;
@@ -14,8 +17,69 @@ internal abstract class TraceLogWriter
1417
{
1518
WriteIndented = true,
1619
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
17-
ReferenceHandler = ReferenceHandler.Preserve
20+
ReferenceHandler = ReferenceHandler.IgnoreCycles,
21+
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
22+
Converters =
23+
{
24+
new JsonStringEnumConverter(),
25+
new ResourceTypeInTraceJsonConverter(),
26+
new ResourceFieldInTraceJsonConverterFactory(),
27+
new IdentifiableInTraceJsonConverter()
28+
}
1829
};
30+
31+
private sealed class ResourceTypeInTraceJsonConverter : JsonConverter<ResourceType>
32+
{
33+
public override ResourceType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
34+
{
35+
throw new NotSupportedException();
36+
}
37+
38+
public override void Write(Utf8JsonWriter writer, ResourceType value, JsonSerializerOptions options)
39+
{
40+
writer.WriteStringValue(value.PublicName);
41+
}
42+
}
43+
44+
private sealed class ResourceFieldInTraceJsonConverterFactory : JsonConverterFactory
45+
{
46+
public override bool CanConvert(Type typeToConvert)
47+
{
48+
return typeToConvert.IsAssignableTo(typeof(ResourceFieldAttribute));
49+
}
50+
51+
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
52+
{
53+
return new ResourceFieldInTraceJsonConverter();
54+
}
55+
56+
private sealed class ResourceFieldInTraceJsonConverter : JsonConverter<ResourceFieldAttribute>
57+
{
58+
public override ResourceFieldAttribute Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
59+
{
60+
throw new NotSupportedException();
61+
}
62+
63+
public override void Write(Utf8JsonWriter writer, ResourceFieldAttribute value, JsonSerializerOptions options)
64+
{
65+
writer.WriteStringValue(value.PublicName);
66+
}
67+
}
68+
}
69+
70+
private sealed class IdentifiableInTraceJsonConverter : JsonConverter<IIdentifiable>
71+
{
72+
public override IIdentifiable Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
73+
{
74+
throw new NotSupportedException();
75+
}
76+
77+
public override void Write(Utf8JsonWriter writer, IIdentifiable value, JsonSerializerOptions options)
78+
{
79+
Type runtimeType = value.GetType();
80+
JsonSerializer.Serialize(writer, value, runtimeType, options);
81+
}
82+
}
1983
}
2084

2185
internal sealed class TraceLogWriter<T> : TraceLogWriter
@@ -88,26 +152,12 @@ private static void WriteProperty(StringBuilder builder, PropertyInfo property,
88152
builder.Append(": ");
89153

90154
object? value = property.GetValue(instance);
91-
92-
if (value == null)
93-
{
94-
builder.Append("null");
95-
}
96-
else if (value is string stringValue)
97-
{
98-
builder.Append('"');
99-
builder.Append(stringValue);
100-
builder.Append('"');
101-
}
102-
else
103-
{
104-
WriteObject(builder, value);
105-
}
155+
WriteObject(builder, value);
106156
}
107157

108-
private static void WriteObject(StringBuilder builder, object value)
158+
private static void WriteObject(StringBuilder builder, object? value)
109159
{
110-
if (HasToStringOverload(value.GetType()))
160+
if (value != null && value is not string && HasToStringOverload(value.GetType()))
111161
{
112162
builder.Append(value);
113163
}
@@ -118,28 +168,19 @@ private static void WriteObject(StringBuilder builder, object value)
118168
}
119169
}
120170

121-
private static bool HasToStringOverload(Type? type)
171+
private static bool HasToStringOverload(Type type)
122172
{
123-
if (type != null)
124-
{
125-
MethodInfo? toStringMethod = type.GetMethod("ToString", Array.Empty<Type>());
126-
127-
if (toStringMethod != null && toStringMethod.DeclaringType != typeof(object))
128-
{
129-
return true;
130-
}
131-
}
132-
133-
return false;
173+
MethodInfo? toStringMethod = type.GetMethod("ToString", Array.Empty<Type>());
174+
return toStringMethod != null && toStringMethod.DeclaringType != typeof(object);
134175
}
135176

136-
private static string SerializeObject(object value)
177+
private static string SerializeObject(object? value)
137178
{
138179
try
139180
{
140181
return JsonSerializer.Serialize(value, SerializerOptions);
141182
}
142-
catch (JsonException)
183+
catch (Exception exception) when (exception is JsonException or NotSupportedException)
143184
{
144185
// Never crash as a result of logging, this is best-effort only.
145186
return "object";

src/JsonApiDotNetCore/Queries/QueryLayer.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ internal void WriteLayer(IndentingStringWriter writer, string? prefix)
4242

4343
using (writer.Indent())
4444
{
45-
if (Include != null)
45+
if (Include != null && Include.Elements.Any())
4646
{
4747
writer.WriteLine($"{nameof(Include)}: {Include}");
4848
}

test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicLoggingTests.cs

+6-4
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,10 @@ public async Task Logs_at_error_level_on_unhandled_exception()
8080
error.Source.ShouldNotBeNull();
8181
error.Source.Pointer.Should().Be("/atomic:operations[0]");
8282

83-
loggerFactory.Logger.Messages.ShouldNotBeEmpty();
83+
IReadOnlyList<FakeLogMessage> logMessages = loggerFactory.Logger.GetMessages();
84+
logMessages.ShouldNotBeEmpty();
8485

85-
loggerFactory.Logger.Messages.Should().ContainSingle(message => message.LogLevel == LogLevel.Error &&
86+
logMessages.Should().ContainSingle(message => message.LogLevel == LogLevel.Error &&
8687
message.Text.Contains("Simulated failure.", StringComparison.Ordinal));
8788
}
8889

@@ -117,9 +118,10 @@ public async Task Logs_at_info_level_on_invalid_request_body()
117118

118119
responseDocument.Errors.ShouldHaveCount(1);
119120

120-
loggerFactory.Logger.Messages.ShouldNotBeEmpty();
121+
IReadOnlyList<FakeLogMessage> logMessages = loggerFactory.Logger.GetMessages();
122+
logMessages.ShouldNotBeEmpty();
121123

122-
loggerFactory.Logger.Messages.Should().ContainSingle(message => message.LogLevel == LogLevel.Information &&
124+
logMessages.Should().ContainSingle(message => message.LogLevel == LogLevel.Information &&
123125
message.Text.Contains("Failed to deserialize request body", StringComparison.Ordinal));
124126
}
125127

0 commit comments

Comments
 (0)