9
9
using System . IO ;
10
10
using System . Text ;
11
11
using System . Text . Json ;
12
+ using System . Threading ;
12
13
using System . Threading . Tasks ;
13
14
using Microsoft . AspNetCore . Http ;
15
+ using Microsoft . AspNetCore . Http . Features ;
14
16
using Microsoft . AspNetCore . Http . Metadata ;
15
17
using Microsoft . Extensions . DependencyInjection ;
18
+ using Microsoft . Extensions . Logging ;
19
+ using Microsoft . Extensions . Logging . Testing ;
16
20
using Microsoft . Extensions . Primitives ;
17
21
using Xunit ;
18
22
@@ -24,6 +28,7 @@ public class MapActionExpressionTreeBuilderTest
24
28
public async Task RequestDelegateInvokesAction ( )
25
29
{
26
30
var invoked = false ;
31
+
27
32
void TestAction ( )
28
33
{
29
34
invoked = true ;
@@ -87,6 +92,10 @@ public async Task UsesDefaultValueIfNoMatchingRouteValue()
87
92
{
88
93
const string unmatchedName = "value" ;
89
94
const int unmatchedRouteParam = 42 ;
95
+ var structToBeZeroed = new BodyStruct
96
+ {
97
+ Id = 42
98
+ } ;
90
99
91
100
int ? deserializedRouteParam = null ;
92
101
@@ -246,6 +255,42 @@ void TestAction([FromBody(AllowEmpty = true)] BodyStruct bodyStruct)
246
255
Assert . Equal ( default , structToBeZeroed ) ;
247
256
}
248
257
258
+ [ Fact ]
259
+ public async Task RequestDelegateLogsFromBodyIOExceptionsAsDebug ( )
260
+ {
261
+ var invoked = false ;
262
+
263
+ var sink = new TestSink ( context => context . LoggerName == typeof ( MapActionExpressionTreeBuilder ) . FullName ) ;
264
+ var testLoggerFactory = new TestLoggerFactory ( sink , enabled : true ) ;
265
+
266
+ void TestAction ( [ FromBody ] Todo todo )
267
+ {
268
+ invoked = true ;
269
+ }
270
+
271
+ var ioException = new IOException ( ) ;
272
+ var serviceCollection = new ServiceCollection ( ) ;
273
+ serviceCollection . AddSingleton < ILoggerFactory > ( testLoggerFactory ) ;
274
+
275
+ var httpContext = new DefaultHttpContext ( ) ;
276
+ httpContext . Request . Headers [ "Content-Type" ] = "application/json" ;
277
+ httpContext . Request . Body = new IOExceptionThrowingRequestBodyStream ( ioException ) ;
278
+ httpContext . Features . Set < IHttpRequestLifetimeFeature > ( new TestHttpRequestLifetimeFeature ( ) ) ;
279
+ httpContext . RequestServices = serviceCollection . BuildServiceProvider ( ) ;
280
+
281
+ var requestDelegate = MapActionExpressionTreeBuilder . BuildRequestDelegate ( ( Action < Todo > ) TestAction ) ;
282
+
283
+ await requestDelegate ( httpContext ) ;
284
+
285
+ Assert . False ( invoked ) ;
286
+ Assert . True ( httpContext . RequestAborted . IsCancellationRequested ) ;
287
+
288
+ var logMessage = Assert . Single ( sink . Writes ) ;
289
+ Assert . Equal ( new EventId ( 1 , "RequestBodyIOException" ) , logMessage . EventId ) ;
290
+ Assert . Equal ( LogLevel . Debug , logMessage . LogLevel ) ;
291
+ Assert . Same ( ioException , logMessage . Exception ) ;
292
+ }
293
+
249
294
[ Fact ]
250
295
public async Task RequestDelegatePopulatesFromFormParameterBasedOnParameterName ( )
251
296
{
@@ -274,6 +319,42 @@ void TestAction([FromForm] int value)
274
319
Assert . Equal ( originalQueryParam , deserializedRouteParam ) ;
275
320
}
276
321
322
+ [ Fact ]
323
+ public async Task RequestDelegateLogsFromFormIOExceptionsAsDebug ( )
324
+ {
325
+ var invoked = false ;
326
+
327
+ var sink = new TestSink ( context => context . LoggerName == typeof ( MapActionExpressionTreeBuilder ) . FullName ) ;
328
+ var testLoggerFactory = new TestLoggerFactory ( sink , enabled : true ) ;
329
+
330
+ void TestAction ( [ FromForm ] int value )
331
+ {
332
+ invoked = true ;
333
+ }
334
+
335
+ var ioException = new IOException ( ) ;
336
+ var serviceCollection = new ServiceCollection ( ) ;
337
+ serviceCollection . AddSingleton < ILoggerFactory > ( testLoggerFactory ) ;
338
+
339
+ var httpContext = new DefaultHttpContext ( ) ;
340
+ httpContext . Request . Headers [ "Content-Type" ] = "application/x-www-form-urlencoded" ;
341
+ httpContext . Request . Body = new IOExceptionThrowingRequestBodyStream ( ioException ) ;
342
+ httpContext . Features . Set < IHttpRequestLifetimeFeature > ( new TestHttpRequestLifetimeFeature ( ) ) ;
343
+ httpContext . RequestServices = serviceCollection . BuildServiceProvider ( ) ;
344
+
345
+ var requestDelegate = MapActionExpressionTreeBuilder . BuildRequestDelegate ( ( Action < int > ) TestAction ) ;
346
+
347
+ await requestDelegate ( httpContext ) ;
348
+
349
+ Assert . False ( invoked ) ;
350
+ Assert . True ( httpContext . RequestAborted . IsCancellationRequested ) ;
351
+
352
+ var logMessage = Assert . Single ( sink . Writes ) ;
353
+ Assert . Equal ( new EventId ( 1 , "RequestBodyIOException" ) , logMessage . EventId ) ;
354
+ Assert . Equal ( LogLevel . Debug , logMessage . LogLevel ) ;
355
+ Assert . Same ( ioException , logMessage . Exception ) ;
356
+ }
357
+
277
358
[ Fact ]
278
359
public void BuildRequestDelegateThrowsInvalidOperationExceptionGivenBothFromBodyAndFromFormOnDifferentParameters ( )
279
360
{
@@ -463,5 +544,62 @@ public Task ExecuteAsync(HttpContext httpContext)
463
544
return httpContext . Response . WriteAsync ( _resultString ) ;
464
545
}
465
546
}
547
+
548
+ private class IOExceptionThrowingRequestBodyStream : Stream
549
+ {
550
+ private readonly Exception _exceptionToThrow ;
551
+
552
+ public IOExceptionThrowingRequestBodyStream ( Exception exceptionToThrow )
553
+ {
554
+ _exceptionToThrow = exceptionToThrow ;
555
+ }
556
+
557
+ public override bool CanRead => true ;
558
+
559
+ public override bool CanSeek => false ;
560
+
561
+ public override bool CanWrite => false ;
562
+
563
+ public override long Length => throw new NotImplementedException ( ) ;
564
+
565
+ public override long Position { get => throw new NotImplementedException ( ) ; set => throw new NotImplementedException ( ) ; }
566
+
567
+ public override void Flush ( )
568
+ {
569
+ throw new NotImplementedException ( ) ;
570
+ }
571
+
572
+ public override int Read ( byte [ ] buffer , int offset , int count )
573
+ {
574
+ throw _exceptionToThrow ;
575
+ }
576
+
577
+ public override long Seek ( long offset , SeekOrigin origin )
578
+ {
579
+ throw new NotImplementedException ( ) ;
580
+ }
581
+
582
+ public override void SetLength ( long value )
583
+ {
584
+ throw new NotImplementedException ( ) ;
585
+ }
586
+
587
+ public override void Write ( byte [ ] buffer , int offset , int count )
588
+ {
589
+ throw new NotImplementedException ( ) ;
590
+ }
591
+ }
592
+
593
+ private class TestHttpRequestLifetimeFeature : IHttpRequestLifetimeFeature
594
+ {
595
+ private readonly CancellationTokenSource _requestAbortedCts = new CancellationTokenSource ( ) ;
596
+
597
+ public CancellationToken RequestAborted { get => _requestAbortedCts . Token ; set => throw new NotImplementedException ( ) ; }
598
+
599
+ public void Abort ( )
600
+ {
601
+ _requestAbortedCts . Cancel ( ) ;
602
+ }
603
+ }
466
604
}
467
605
}
0 commit comments