5
5
using System . Globalization ;
6
6
using System . IO ;
7
7
using System . Linq ;
8
+ using System . Net . Http ;
8
9
using System . Text ;
9
10
using System . Threading . Tasks ;
10
11
using i18n . Core . Abstractions ;
11
12
using JetBrains . Annotations ;
12
13
using Microsoft . AspNetCore . Hosting ;
13
14
using Microsoft . AspNetCore . Http ;
15
+ using Microsoft . AspNetCore . Http . Features ;
14
16
using Microsoft . AspNetCore . Localization ;
15
17
using Microsoft . Extensions . Hosting ;
16
18
using Microsoft . Extensions . Logging ;
@@ -48,7 +50,6 @@ public sealed class I18NMiddlewareOptions
48
50
public ICollection < string > ExcludeUrls { get ; }
49
51
public bool CacheEnabled { get ; [ UsedImplicitly ] set ; }
50
52
public Encoding RequestEncoding { get ; [ UsedImplicitly ] set ; }
51
- public int RequestBufferingThreshold { get ; [ UsedImplicitly ] set ; }
52
53
53
54
public I18NMiddlewareOptions ( )
54
55
{
@@ -69,7 +70,6 @@ public I18NMiddlewareOptions()
69
70
} ;
70
71
71
72
RequestEncoding = Encoding . UTF8 ;
72
- RequestBufferingThreshold = 84000 ; // Less than default GC LOH
73
73
}
74
74
}
75
75
@@ -82,7 +82,7 @@ public sealed class I18NMiddleware
82
82
readonly INuggetReplacer _nuggetReplacer ;
83
83
readonly I18NMiddlewareOptions _options ;
84
84
85
- public I18NMiddleware ( RequestDelegate next , ILocalizationManager localizationManager , IOptions < I18NMiddlewareOptions > middleWareOptions ,
85
+ public I18NMiddleware ( RequestDelegate next , ILocalizationManager localizationManager , IOptions < I18NMiddlewareOptions > middleWareOptions ,
86
86
[ CanBeNull ] ILogger < I18NMiddleware > logger , IPooledStreamManager pooledStreamManager , INuggetReplacer nuggetReplacer )
87
87
{
88
88
_next = next ;
@@ -100,27 +100,33 @@ public I18NMiddleware(RequestDelegate next, ILocalizationManager localizationMan
100
100
[ UsedImplicitly ]
101
101
public async Task InvokeAsync ( HttpContext context , IWebHostEnvironment webHostEnvironment )
102
102
{
103
+ var cancellationToken = context . RequestAborted ;
104
+ var requestEncoding = _options . RequestEncoding ?? Encoding . UTF8 ;
103
105
var excludeUrls = _options . ExcludeUrls ;
104
- var modifyResponse = excludeUrls == null || ! excludeUrls . Any ( bl => context . Request . Path . Value != null
106
+ var modifyResponse = excludeUrls == null || ! excludeUrls . Any ( bl => context . Request . Path . Value != null
105
107
&& context . Request . Path . Value . ToLowerInvariant ( ) . Contains ( bl ) ) ;
106
108
if ( ! modifyResponse )
107
109
{
108
110
await _next ( context ) ;
109
111
return ;
110
112
}
111
113
112
- context . Request . EnableBuffering ( _options . RequestBufferingThreshold ) ;
113
- var originalResponseBodyStream = ReplaceHttpResponseBodyStream ( context . Response ) ;
114
+ var responseBodyPooledStream = new DisposablePooledStream ( _pooledStreamManager , nameof ( I18NMiddleware ) ) ;
115
+ context . Response . RegisterForDisposeAsync ( responseBodyPooledStream ) ;
114
116
115
- try
116
- {
117
- await _next ( context ) ;
118
- }
119
- catch
120
- {
121
- await ReturnHttpResponseBodyStreamAsync ( context . Response , originalResponseBodyStream ) . ConfigureAwait ( false ) ;
122
- throw ;
123
- }
117
+ var httpResponseBodyFeature = context . Features . Get < IHttpResponseBodyFeature > ( ) ;
118
+ var httpResponseFeature = context . Features . Get < IHttpResponseFeature > ( ) ;
119
+
120
+ var streamResponseBodyFeature = new StreamResponseBodyFeature ( responseBodyPooledStream ) ;
121
+ context . Features . Set < IHttpResponseBodyFeature > ( streamResponseBodyFeature ) ;
122
+
123
+ await _next ( context ) . ConfigureAwait ( false ) ;
124
+
125
+ // Force dynamic content type in order reset Content-Length header.
126
+ httpResponseFeature . Headers . ContentLength = null ;
127
+
128
+ var httpResponseBodyStream = ( Stream ) responseBodyPooledStream ;
129
+ httpResponseBodyStream . Seek ( 0 , SeekOrigin . Begin ) ;
124
130
125
131
var contentType = GetRequestContentType ( context ) ;
126
132
var validContentTypes = _options . ValidContentTypes ;
@@ -133,8 +139,8 @@ public async Task InvokeAsync(HttpContext context, IWebHostEnvironment webHostEn
133
139
_logger ? . LogDebug (
134
140
$ "Request path: { context . Request . Path } . Culture name: { cultureDictionary . CultureName } . Translations: { cultureDictionary . Translations . Count } .") ;
135
141
136
- var responseBody = await ReadResponseBodyAsStringAsync ( context ) ;
137
-
142
+ var responseBody = await ReadResponseBodyAsStringAsync ( httpResponseBodyStream , requestEncoding ) ;
143
+
138
144
string responseBodyTranslated ;
139
145
if ( webHostEnvironment . IsDevelopment ( ) )
140
146
{
@@ -145,20 +151,20 @@ public async Task InvokeAsync(HttpContext context, IWebHostEnvironment webHostEn
145
151
146
152
_logger ? . LogDebug ( $ "Replaced body in { sw . ElapsedMilliseconds } ms.") ;
147
153
const string i18NMiddlewareName = "X-" + nameof ( I18NMiddleware ) + "-Ms" ;
148
- context . Response . Headers [ i18NMiddlewareName ] = sw . ElapsedMilliseconds . ToString ( ) ;
154
+ httpResponseFeature . Headers [ i18NMiddlewareName ] = sw . ElapsedMilliseconds . ToString ( ) ;
149
155
}
150
156
else
151
157
{
152
158
responseBodyTranslated = _nuggetReplacer . Replace ( cultureDictionary , responseBody ) ;
153
159
}
154
160
155
- context . Response . Body = originalResponseBodyStream ;
156
- await context . Response . WriteAsync ( responseBodyTranslated , _options . RequestEncoding ?? Encoding . UTF8 ) ;
161
+ var stringContent = new StringContent ( responseBodyTranslated , requestEncoding , contentType ) ;
162
+ await stringContent . CopyToAsync ( httpResponseBodyFeature . Stream , cancellationToken ) ;
157
163
158
164
return ;
159
165
}
160
166
161
- await ReturnHttpResponseBodyStreamAsync ( context . Response , originalResponseBodyStream ) . ConfigureAwait ( false ) ;
167
+ await httpResponseBodyStream . CopyToAsync ( httpResponseBodyFeature . Stream , cancellationToken ) . ConfigureAwait ( false ) ;
162
168
}
163
169
164
170
[ SuppressMessage ( "ReSharper" , "ConstantConditionalAccessQualifier" ) ]
@@ -186,47 +192,23 @@ CultureInfo GetRequestCultureInfo(HttpContext context)
186
192
return requestCultureInfo ;
187
193
}
188
194
189
- static async Task < string > ReadResponseBodyAsStringAsync ( HttpContext context )
195
+ static async Task < string > ReadResponseBodyAsStringAsync ( Stream stream , Encoding encoding )
190
196
{
191
- context . Response . Body . Seek ( 0 , SeekOrigin . Begin ) ;
192
-
193
- string responseBody ;
194
- using ( var streamReader = new StreamReader ( context . Response . Body , Encoding . UTF8 , false , leaveOpen : true ) )
195
- {
196
- responseBody = await streamReader . ReadToEndAsync ( ) . ConfigureAwait ( false ) ;
197
- }
198
-
199
- context . Response . Body . Seek ( 0 , SeekOrigin . Begin ) ;
200
-
201
- return responseBody ;
202
- }
203
-
204
- Stream ReplaceHttpResponseBodyStream ( HttpResponse httpResponse )
205
- {
206
- var originBody = httpResponse . Body ;
207
- httpResponse . Body = _pooledStreamManager . GetStream ( nameof ( I18NMiddleware ) ) ??
208
- throw new Exception ( $ "{ nameof ( _pooledStreamManager ) } must return a valid stream.") ;
209
- httpResponse . Body . Seek ( 0 , SeekOrigin . Begin ) ;
210
- httpResponse . RegisterForDisposeAsync ( new DisposablePooledStream ( _pooledStreamManager , httpResponse . Body ) ) ;
211
- return originBody ;
197
+ using var streamReader = new StreamReader ( stream , encoding , false ) ;
198
+ return await streamReader . ReadToEndAsync ( ) . ConfigureAwait ( false ) ;
212
199
}
213
200
214
- static async ValueTask ReturnHttpResponseBodyStreamAsync ( HttpResponse httpResponse , Stream originalBodyResponseStream )
215
- {
216
- httpResponse . Body . Seek ( 0 , SeekOrigin . Begin ) ;
217
- await httpResponse . Body . CopyToAsync ( originalBodyResponseStream ) . ConfigureAwait ( false ) ;
218
- httpResponse . Body = originalBodyResponseStream ;
219
- }
220
-
221
201
readonly struct DisposablePooledStream : IAsyncDisposable
222
202
{
223
203
readonly IPooledStreamManager _pooledStreamManager ;
224
204
readonly Stream _stream ;
225
205
226
- public DisposablePooledStream ( IPooledStreamManager pooledStreamManager , Stream stream )
206
+ public static implicit operator Stream ( DisposablePooledStream stream ) => stream . _stream ;
207
+
208
+ public DisposablePooledStream ( IPooledStreamManager pooledStreamManager , string streamName )
227
209
{
228
210
_pooledStreamManager = pooledStreamManager ;
229
- _stream = stream ;
211
+ _stream = _pooledStreamManager . GetStream ( streamName ) ;
230
212
}
231
213
232
214
public ValueTask DisposeAsync ( )
0 commit comments