4
4
using System . Net ;
5
5
using System . Net . Http ;
6
6
using System . Text ;
7
+ using System . Text . Json ;
7
8
using System . Threading . Tasks ;
9
+ using JsonApiDotNetCore . Configuration ;
8
10
using JsonApiDotNetCore . Diagnostics ;
9
11
using JsonApiDotNetCore . Errors ;
10
12
using JsonApiDotNetCore . Middleware ;
@@ -21,19 +23,26 @@ namespace JsonApiDotNetCore.Serialization
21
23
/// <inheritdoc />
22
24
public sealed class JsonApiWriter : IJsonApiWriter
23
25
{
24
- private readonly IJsonApiSerializer _serializer ;
26
+ private readonly IJsonApiRequest _request ;
27
+ private readonly IJsonApiOptions _options ;
28
+ private readonly IResponseModelAdapter _responseModelAdapter ;
25
29
private readonly IExceptionHandler _exceptionHandler ;
26
30
private readonly IETagGenerator _eTagGenerator ;
27
31
private readonly TraceLogWriter < JsonApiWriter > _traceWriter ;
28
32
29
- public JsonApiWriter ( IJsonApiSerializer serializer , IExceptionHandler exceptionHandler , IETagGenerator eTagGenerator , ILoggerFactory loggerFactory )
33
+ public JsonApiWriter ( IJsonApiRequest request , IJsonApiOptions options , IResponseModelAdapter responseModelAdapter , IExceptionHandler exceptionHandler ,
34
+ IETagGenerator eTagGenerator , ILoggerFactory loggerFactory )
30
35
{
31
- ArgumentGuard . NotNull ( serializer , nameof ( serializer ) ) ;
36
+ ArgumentGuard . NotNull ( request , nameof ( request ) ) ;
37
+ ArgumentGuard . NotNull ( responseModelAdapter , nameof ( responseModelAdapter ) ) ;
32
38
ArgumentGuard . NotNull ( exceptionHandler , nameof ( exceptionHandler ) ) ;
33
39
ArgumentGuard . NotNull ( eTagGenerator , nameof ( eTagGenerator ) ) ;
40
+ ArgumentGuard . NotNull ( options , nameof ( options ) ) ;
34
41
ArgumentGuard . NotNull ( loggerFactory , nameof ( loggerFactory ) ) ;
35
42
36
- _serializer = serializer ;
43
+ _request = request ;
44
+ _options = options ;
45
+ _responseModelAdapter = responseModelAdapter ;
37
46
_exceptionHandler = exceptionHandler ;
38
47
_eTagGenerator = eTagGenerator ;
39
48
_traceWriter = new TraceLogWriter < JsonApiWriter > ( loggerFactory ) ;
@@ -44,83 +53,84 @@ public async Task WriteAsync(object model, HttpContext httpContext)
44
53
{
45
54
ArgumentGuard . NotNull ( httpContext , nameof ( httpContext ) ) ;
46
55
47
- HttpRequest request = httpContext . Request ;
48
- HttpResponse response = httpContext . Response ;
56
+ if ( model == null && ! CanWriteBody ( ( HttpStatusCode ) httpContext . Response . StatusCode ) )
57
+ {
58
+ // Prevent exception from Kestrel server, caused by writing data:null json response.
59
+ return ;
60
+ }
49
61
50
- await using TextWriter writer = new HttpResponseStreamWriter ( response . Body , Encoding . UTF8 ) ;
51
- string responseContent ;
62
+ string responseBody = GetResponseBody ( model , httpContext ) ;
63
+
64
+ _traceWriter . LogMessage ( ( ) =>
65
+ $ "Sending { httpContext . Response . StatusCode } response for { httpContext . Request . Method } request at '{ httpContext . Request . GetEncodedUrl ( ) } ' with body: <<{ responseBody } >>") ;
52
66
53
- using ( IDisposable _ = CodeTimingSessionManager . Current . Measure ( "Write response body" ) )
67
+ await SendResponseBodyAsync ( httpContext . Response , responseBody ) ;
68
+ }
69
+
70
+ private static bool CanWriteBody ( HttpStatusCode statusCode )
71
+ {
72
+ return statusCode is not HttpStatusCode . NoContent and not HttpStatusCode . ResetContent and not HttpStatusCode . NotModified ;
73
+ }
74
+
75
+ private string GetResponseBody ( object model , HttpContext httpContext )
76
+ {
77
+ using IDisposable _ = CodeTimingSessionManager . Current . Measure ( "Write response body" ) ;
78
+
79
+ try
54
80
{
55
- try
81
+ if ( model is ProblemDetails problemDetails )
56
82
{
57
- responseContent = SerializeResponse ( model , ( HttpStatusCode ) response . StatusCode ) ;
83
+ throw new UnsuccessfulActionResultException ( problemDetails ) ;
58
84
}
59
- #pragma warning disable AV1210 // Catch a specific exception instead of Exception, SystemException or ApplicationException
60
- catch ( Exception exception )
61
- #pragma warning restore AV1210 // Catch a specific exception instead of Exception, SystemException or ApplicationException
85
+
86
+ if ( model == null && ! IsSuccessStatusCode ( ( HttpStatusCode ) httpContext . Response . StatusCode ) )
62
87
{
63
- IReadOnlyList < ErrorObject > errors = _exceptionHandler . HandleException ( exception ) ;
64
- responseContent = _serializer . Serialize ( errors ) ;
65
- response . StatusCode = ( int ) ErrorObject . GetResponseStatusCode ( errors ) ;
88
+ throw new UnsuccessfulActionResultException ( ( HttpStatusCode ) httpContext . Response . StatusCode ) ;
66
89
}
67
90
68
- bool hasMatchingETag = SetETagResponseHeader ( request , response , responseContent ) ;
91
+ string responseBody = RenderModel ( model ) ;
69
92
70
- if ( hasMatchingETag )
93
+ if ( SetETagResponseHeader ( httpContext . Request , httpContext . Response , responseBody ) )
71
94
{
72
- response . StatusCode = ( int ) HttpStatusCode . NotModified ;
73
- responseContent = string . Empty ;
95
+ httpContext . Response . StatusCode = ( int ) HttpStatusCode . NotModified ;
96
+ return null ;
74
97
}
75
98
76
- if ( request . Method == HttpMethod . Head . Method )
99
+ if ( httpContext . Request . Method == HttpMethod . Head . Method )
77
100
{
78
- responseContent = string . Empty ;
101
+ return null ;
79
102
}
80
103
81
- if ( ! string . IsNullOrEmpty ( responseContent ) )
82
- {
83
- response . ContentType = _serializer . ContentType ;
84
- }
104
+ return responseBody ;
85
105
}
86
-
87
- _traceWriter . LogMessage ( ( ) =>
88
- $ "Sending { response . StatusCode } response for { request . Method } request at '{ request . GetEncodedUrl ( ) } ' with body: <<{ responseContent } >>") ;
89
-
90
- using ( IDisposable _ = CodeTimingSessionManager . Current . Measure ( "Send response body" ) )
106
+ #pragma warning disable AV1210 // Catch a specific exception instead of Exception, SystemException or ApplicationException
107
+ catch ( Exception exception )
108
+ #pragma warning restore AV1210 // Catch a specific exception instead of Exception, SystemException or ApplicationException
91
109
{
92
- await writer . WriteAsync ( responseContent ) ;
93
- await writer . FlushAsync ( ) ;
110
+ IReadOnlyList < ErrorObject > errors = _exceptionHandler . HandleException ( exception ) ;
111
+ httpContext . Response . StatusCode = ( int ) ErrorObject . GetResponseStatusCode ( errors ) ;
112
+
113
+ return RenderModel ( errors ) ;
94
114
}
95
115
}
96
116
97
- private string SerializeResponse ( object contextObject , HttpStatusCode statusCode )
117
+ private static bool IsSuccessStatusCode ( HttpStatusCode statusCode )
98
118
{
99
- if ( contextObject is ProblemDetails problemDetails )
100
- {
101
- throw new UnsuccessfulActionResultException ( problemDetails ) ;
102
- }
103
-
104
- if ( contextObject == null )
105
- {
106
- if ( ! IsSuccessStatusCode ( statusCode ) )
107
- {
108
- throw new UnsuccessfulActionResultException ( statusCode ) ;
109
- }
110
-
111
- if ( statusCode is HttpStatusCode . NoContent or HttpStatusCode . ResetContent or HttpStatusCode . NotModified )
112
- {
113
- // Prevent exception from Kestrel server, caused by writing data:null json response.
114
- return null ;
115
- }
116
- }
119
+ return new HttpResponseMessage ( statusCode ) . IsSuccessStatusCode ;
120
+ }
117
121
118
- return _serializer . Serialize ( contextObject ) ;
122
+ private string RenderModel ( object model )
123
+ {
124
+ Document document = _responseModelAdapter . Convert ( model ) ;
125
+ return SerializeDocument ( document ) ;
119
126
}
120
127
121
- private bool IsSuccessStatusCode ( HttpStatusCode statusCode )
128
+ private string SerializeDocument ( Document document )
122
129
{
123
- return new HttpResponseMessage ( statusCode ) . IsSuccessStatusCode ;
130
+ using IDisposable _ =
131
+ CodeTimingSessionManager . Current . Measure ( "JsonSerializer.Serialize" , MeasurementSettings . ExcludeJsonSerializationInPercentages ) ;
132
+
133
+ return JsonSerializer . Serialize ( document , _options . SerializerWriteOptions ) ;
124
134
}
125
135
126
136
private bool SetETagResponseHeader ( HttpRequest request , HttpResponse response , string responseContent )
@@ -159,5 +169,20 @@ private static bool RequestContainsMatchingETag(IHeaderDictionary requestHeaders
159
169
160
170
return false ;
161
171
}
172
+
173
+ private async Task SendResponseBodyAsync ( HttpResponse httpResponse , string responseBody )
174
+ {
175
+ if ( ! string . IsNullOrEmpty ( responseBody ) )
176
+ {
177
+ httpResponse . ContentType =
178
+ _request . Kind == EndpointKind . AtomicOperations ? HeaderConstants . AtomicOperationsMediaType : HeaderConstants . MediaType ;
179
+
180
+ using IDisposable _ = CodeTimingSessionManager . Current . Measure ( "Send response body" ) ;
181
+
182
+ await using TextWriter writer = new HttpResponseStreamWriter ( httpResponse . Body , Encoding . UTF8 ) ;
183
+ await writer . WriteAsync ( responseBody ) ;
184
+ await writer . FlushAsync ( ) ;
185
+ }
186
+ }
162
187
}
163
188
}
0 commit comments