@@ -499,6 +499,53 @@ public static bool TryParse(string? value, out MyTryParsableRecord? result)
499
499
}
500
500
}
501
501
502
+ private class MyBindAsyncTypeThatThrows
503
+ {
504
+ public static ValueTask < object ? > BindAsync ( HttpContext context )
505
+ {
506
+ throw new InvalidOperationException ( "BindAsync failed" ) ;
507
+ }
508
+ }
509
+
510
+ private record MyBindAsyncRecord ( Uri Uri )
511
+ {
512
+ public static ValueTask < object ? > BindAsync ( HttpContext context )
513
+ {
514
+ if ( ! Uri . TryCreate ( context . Request . Headers . Referer , UriKind . Absolute , out var uri ) )
515
+ {
516
+ return ValueTask . FromResult < object ? > ( null ) ;
517
+ }
518
+
519
+ return ValueTask . FromResult < object ? > ( new MyBindAsyncRecord ( uri ) ) ;
520
+ }
521
+
522
+ // TryParse(HttpContext, ...) should be preferred over TryParse(string, ...) if there's
523
+ // no [FromRoute] or [FromQuery] attributes.
524
+ public static bool TryParse ( string ? value , out MyBindAsyncRecord ? result )
525
+ {
526
+ throw new NotImplementedException ( ) ;
527
+ }
528
+ }
529
+
530
+ private record struct MyBindAsyncStruct ( Uri Uri )
531
+ {
532
+ public static ValueTask < object ? > BindAsync ( HttpContext context )
533
+ {
534
+ if ( ! Uri . TryCreate ( context . Request . Headers . Referer , UriKind . Absolute , out var uri ) )
535
+ {
536
+ return ValueTask . FromResult < object ? > ( null ) ;
537
+ }
538
+
539
+ return ValueTask . FromResult < object ? > ( new MyBindAsyncStruct ( uri ) ) ;
540
+ }
541
+
542
+ // TryParse(HttpContext, ...) should be preferred over TryParse(string, ...) if there's
543
+ // no [FromRoute] or [FromQuery] attributes.
544
+ public static bool TryParse ( string ? value , out MyBindAsyncStruct result ) =>
545
+ throw new NotImplementedException ( ) ;
546
+ }
547
+
548
+
502
549
[ Theory ]
503
550
[ MemberData ( nameof ( TryParsableParameters ) ) ]
504
551
public async Task RequestDelegatePopulatesUnattributedTryParsableParametersFromRouteValue ( Delegate action , string ? routeValue , object ? expectedParameterValue )
@@ -560,6 +607,84 @@ public async Task RequestDelegatePopulatesUnattributedTryParsableParametersFromR
560
607
Assert . Equal ( 42 , httpContext . Items [ "tryParsable" ] ) ;
561
608
}
562
609
610
+ [ Fact ]
611
+ public async Task RequestDelegatePrefersBindAsyncOverTryParseString ( )
612
+ {
613
+ var httpContext = new DefaultHttpContext ( ) ;
614
+
615
+ httpContext . Request . Headers . Referer = "https://example.org" ;
616
+
617
+ var requestDelegate = RequestDelegateFactory . Create ( ( HttpContext httpContext , MyBindAsyncRecord tryParsable ) =>
618
+ {
619
+ httpContext . Items [ "tryParsable" ] = tryParsable ;
620
+ } ) ;
621
+
622
+ await requestDelegate ( httpContext ) ;
623
+
624
+ Assert . Equal ( new MyBindAsyncRecord ( new Uri ( "https://example.org" ) ) , httpContext . Items [ "tryParsable" ] ) ;
625
+ }
626
+
627
+ [ Fact ]
628
+ public async Task RequestDelegatePrefersBindAsyncOverTryParseStringForNonNullableStruct ( )
629
+ {
630
+ var httpContext = new DefaultHttpContext ( ) ;
631
+
632
+ httpContext . Request . Headers . Referer = "https://example.org" ;
633
+
634
+ var requestDelegate = RequestDelegateFactory . Create ( ( HttpContext httpContext , MyBindAsyncStruct tryParsable ) =>
635
+ {
636
+ httpContext . Items [ "tryParsable" ] = tryParsable ;
637
+ } ) ;
638
+
639
+ await requestDelegate ( httpContext ) ;
640
+
641
+ Assert . Equal ( new MyBindAsyncStruct ( new Uri ( "https://example.org" ) ) , httpContext . Items [ "tryParsable" ] ) ;
642
+ }
643
+
644
+ [ Fact ]
645
+ public async Task RequestDelegateUsesTryParseStringoOverBindAsyncGivenExplicitAttribute ( )
646
+ {
647
+ var fromRouteRequestDelegate = RequestDelegateFactory . Create ( ( HttpContext httpContext , [ FromRoute ] MyBindAsyncRecord tryParsable ) => { } ) ;
648
+ var fromQueryRequestDelegate = RequestDelegateFactory . Create ( ( HttpContext httpContext , [ FromQuery ] MyBindAsyncRecord tryParsable ) => { } ) ;
649
+
650
+ var httpContext = new DefaultHttpContext
651
+ {
652
+ Request =
653
+ {
654
+ RouteValues =
655
+ {
656
+ [ "tryParsable" ] = "foo"
657
+ } ,
658
+ Query = new QueryCollection ( new Dictionary < string , StringValues >
659
+ {
660
+ [ "tryParsable" ] = "foo"
661
+ } ) ,
662
+ } ,
663
+ } ;
664
+
665
+ await Assert . ThrowsAsync < NotImplementedException > ( ( ) => fromRouteRequestDelegate ( httpContext ) ) ;
666
+ await Assert . ThrowsAsync < NotImplementedException > ( ( ) => fromQueryRequestDelegate ( httpContext ) ) ;
667
+ }
668
+
669
+ [ Fact ]
670
+ public async Task RequestDelegateUsesTryParseStringOverBindAsyncGivenNullableStruct ( )
671
+ {
672
+ var fromRouteRequestDelegate = RequestDelegateFactory . Create ( ( HttpContext httpContext , MyBindAsyncStruct ? tryParsable ) => { } ) ;
673
+
674
+ var httpContext = new DefaultHttpContext
675
+ {
676
+ Request =
677
+ {
678
+ RouteValues =
679
+ {
680
+ [ "tryParsable" ] = "foo"
681
+ } ,
682
+ } ,
683
+ } ;
684
+
685
+ await Assert . ThrowsAsync < NotImplementedException > ( ( ) => fromRouteRequestDelegate ( httpContext ) ) ;
686
+ }
687
+
563
688
public static object [ ] [ ] DelegatesWithAttributesOnNotTryParsableParameters
564
689
{
565
690
get
@@ -629,11 +754,169 @@ void TestAction([FromRoute] int tryParsable, [FromRoute] int tryParsable2)
629
754
Assert . Equal ( LogLevel . Debug , logs [ 0 ] . LogLevel ) ;
630
755
Assert . Equal ( @"Failed to bind parameter ""Int32 tryParsable"" from ""invalid!""." , logs [ 0 ] . Message ) ;
631
756
632
- Assert . Equal ( new EventId ( 3 , "ParamaterBindingFailed" ) , logs [ 0 ] . EventId ) ;
633
- Assert . Equal ( LogLevel . Debug , logs [ 0 ] . LogLevel ) ;
757
+ Assert . Equal ( new EventId ( 3 , "ParamaterBindingFailed" ) , logs [ 1 ] . EventId ) ;
758
+ Assert . Equal ( LogLevel . Debug , logs [ 1 ] . LogLevel ) ;
634
759
Assert . Equal ( @"Failed to bind parameter ""Int32 tryParsable2"" from ""invalid again!""." , logs [ 1 ] . Message ) ;
635
760
}
636
761
762
+ [ Fact ]
763
+ public async Task RequestDelegateLogsBindAsyncFailuresAndSets400Response ( )
764
+ {
765
+ // Not supplying any headers will cause the HttpContext TryParse overload to fail.
766
+ var httpContext = new DefaultHttpContext ( )
767
+ {
768
+ RequestServices = new ServiceCollection ( ) . AddSingleton ( LoggerFactory ) . BuildServiceProvider ( ) ,
769
+ } ;
770
+
771
+ var invoked = false ;
772
+
773
+ var requestDelegate = RequestDelegateFactory . Create ( ( MyBindAsyncRecord arg1 , MyBindAsyncRecord arg2 ) =>
774
+ {
775
+ invoked = true ;
776
+ } ) ;
777
+
778
+ await requestDelegate ( httpContext ) ;
779
+
780
+ Assert . False ( invoked ) ;
781
+ Assert . False ( httpContext . RequestAborted . IsCancellationRequested ) ;
782
+ Assert . Equal ( 400 , httpContext . Response . StatusCode ) ;
783
+
784
+ var logs = TestSink . Writes . ToArray ( ) ;
785
+
786
+ Assert . Equal ( 2 , logs . Length ) ;
787
+
788
+ Assert . Equal ( new EventId ( 4 , "RequiredParameterNotProvided" ) , logs [ 0 ] . EventId ) ;
789
+ Assert . Equal ( LogLevel . Debug , logs [ 0 ] . LogLevel ) ;
790
+ Assert . Equal ( @"Required parameter ""MyBindAsyncRecord arg1"" was not provided." , logs [ 0 ] . Message ) ;
791
+
792
+ Assert . Equal ( new EventId ( 4 , "RequiredParameterNotProvided" ) , logs [ 1 ] . EventId ) ;
793
+ Assert . Equal ( LogLevel . Debug , logs [ 1 ] . LogLevel ) ;
794
+ Assert . Equal ( @"Required parameter ""MyBindAsyncRecord arg2"" was not provided." , logs [ 1 ] . Message ) ;
795
+ }
796
+
797
+ [ Fact ]
798
+ public async Task BindAsyncExceptionsThrowException ( )
799
+ {
800
+ // Not supplying any headers will cause the HttpContext TryParse overload to fail.
801
+ var httpContext = new DefaultHttpContext ( )
802
+ {
803
+ RequestServices = new ServiceCollection ( ) . AddSingleton ( LoggerFactory ) . BuildServiceProvider ( ) ,
804
+ } ;
805
+
806
+ var requestDelegate = RequestDelegateFactory . Create ( ( MyBindAsyncTypeThatThrows arg1 ) => { } ) ;
807
+
808
+ var ex = await Assert . ThrowsAsync < InvalidOperationException > ( ( ) => requestDelegate ( httpContext ) ) ;
809
+ Assert . Equal ( "BindAsync failed" , ex . Message ) ;
810
+ }
811
+
812
+ [ Fact ]
813
+ public async Task BindAsyncWithBodyArgument ( )
814
+ {
815
+ Todo originalTodo = new ( )
816
+ {
817
+ Name = "Write more tests!"
818
+ } ;
819
+
820
+ var httpContext = new DefaultHttpContext ( ) ;
821
+
822
+ var requestBodyBytes = JsonSerializer . SerializeToUtf8Bytes ( originalTodo ) ;
823
+ var stream = new MemoryStream ( requestBodyBytes ) ; ;
824
+ httpContext . Request . Body = stream ;
825
+
826
+ httpContext . Request . Headers [ "Content-Type" ] = "application/json" ;
827
+ httpContext . Request . Headers [ "Content-Length" ] = stream . Length . ToString ( ) ;
828
+ httpContext . Features . Set < IHttpRequestBodyDetectionFeature > ( new RequestBodyDetectionFeature ( true ) ) ;
829
+
830
+ var jsonOptions = new JsonOptions ( ) ;
831
+ jsonOptions . SerializerOptions . Converters . Add ( new TodoJsonConverter ( ) ) ;
832
+
833
+ var mock = new Mock < IServiceProvider > ( ) ;
834
+ mock . Setup ( m => m . GetService ( It . IsAny < Type > ( ) ) ) . Returns < Type > ( t =>
835
+ {
836
+ if ( t == typeof ( IOptions < JsonOptions > ) )
837
+ {
838
+ return Options . Create ( jsonOptions ) ;
839
+ }
840
+ return null ;
841
+ } ) ;
842
+
843
+ httpContext . RequestServices = mock . Object ;
844
+ httpContext . Request . Headers . Referer = "https://example.org" ;
845
+
846
+ var invoked = false ;
847
+
848
+ var requestDelegate = RequestDelegateFactory . Create ( ( HttpContext context , MyBindAsyncRecord arg1 , Todo todo ) =>
849
+ {
850
+ invoked = true ;
851
+ context . Items [ nameof ( arg1 ) ] = arg1 ;
852
+ context . Items [ nameof ( todo ) ] = todo ;
853
+ } ) ;
854
+
855
+ await requestDelegate ( httpContext ) ;
856
+
857
+ Assert . True ( invoked ) ;
858
+ var arg = httpContext . Items [ "arg1" ] as MyBindAsyncRecord ;
859
+ Assert . NotNull ( arg ) ;
860
+ Assert . Equal ( "https://example.org/" , arg ! . Uri . ToString ( ) ) ;
861
+ var todo = httpContext . Items [ "todo" ] as Todo ;
862
+ Assert . NotNull ( todo ) ;
863
+ Assert . Equal ( "Write more tests!" , todo ! . Name ) ;
864
+ }
865
+
866
+ [ Fact ]
867
+ public async Task BindAsyncRunsBeforeBodyBinding ( )
868
+ {
869
+ Todo originalTodo = new ( )
870
+ {
871
+ Name = "Write more tests!"
872
+ } ;
873
+
874
+ var httpContext = new DefaultHttpContext ( ) ;
875
+
876
+ var requestBodyBytes = JsonSerializer . SerializeToUtf8Bytes ( originalTodo ) ;
877
+ var stream = new MemoryStream ( requestBodyBytes ) ; ;
878
+ httpContext . Request . Body = stream ;
879
+
880
+ httpContext . Request . Headers [ "Content-Type" ] = "application/json" ;
881
+ httpContext . Request . Headers [ "Content-Length" ] = stream . Length . ToString ( ) ;
882
+ httpContext . Features . Set < IHttpRequestBodyDetectionFeature > ( new RequestBodyDetectionFeature ( true ) ) ;
883
+
884
+ var jsonOptions = new JsonOptions ( ) ;
885
+ jsonOptions . SerializerOptions . Converters . Add ( new TodoJsonConverter ( ) ) ;
886
+
887
+ var mock = new Mock < IServiceProvider > ( ) ;
888
+ mock . Setup ( m => m . GetService ( It . IsAny < Type > ( ) ) ) . Returns < Type > ( t =>
889
+ {
890
+ if ( t == typeof ( IOptions < JsonOptions > ) )
891
+ {
892
+ return Options . Create ( jsonOptions ) ;
893
+ }
894
+ return null ;
895
+ } ) ;
896
+
897
+ httpContext . RequestServices = mock . Object ;
898
+ httpContext . Request . Headers . Referer = "https://example.org" ;
899
+
900
+ var invoked = false ;
901
+
902
+ var requestDelegate = RequestDelegateFactory . Create ( ( HttpContext context , CustomTodo customTodo , Todo todo ) =>
903
+ {
904
+ invoked = true ;
905
+ context . Items [ nameof ( customTodo ) ] = customTodo ;
906
+ context . Items [ nameof ( todo ) ] = todo ;
907
+ } ) ;
908
+
909
+ await requestDelegate ( httpContext ) ;
910
+
911
+ Assert . True ( invoked ) ;
912
+ var todo0 = httpContext . Items [ "customTodo" ] as Todo ;
913
+ Assert . NotNull ( todo0 ) ;
914
+ Assert . Equal ( "Write more tests!" , todo0 ! . Name ) ;
915
+ var todo1 = httpContext . Items [ "todo" ] as Todo ;
916
+ Assert . NotNull ( todo1 ) ;
917
+ Assert . Equal ( "Write more tests!" , todo1 ! . Name ) ;
918
+ }
919
+
637
920
[ Fact ]
638
921
public async Task RequestDelegatePopulatesFromQueryParameterBasedOnParameterName ( )
639
922
{
@@ -1669,6 +1952,26 @@ public async Task RequestDelegateHandlesBodyParamOptionality(Delegate @delegate,
1669
1952
}
1670
1953
}
1671
1954
1955
+ [ Fact ]
1956
+ public async Task RequestDelegateDoesSupportBindAsyncOptionality ( )
1957
+ {
1958
+ var httpContext = new DefaultHttpContext ( )
1959
+ {
1960
+ RequestServices = new ServiceCollection ( ) . AddSingleton ( LoggerFactory ) . BuildServiceProvider ( ) ,
1961
+ } ;
1962
+
1963
+ var invoked = false ;
1964
+
1965
+ var requestDelegate = RequestDelegateFactory . Create ( ( MyBindAsyncRecord ? arg1 ) =>
1966
+ {
1967
+ invoked = true ;
1968
+ } ) ;
1969
+
1970
+ await requestDelegate ( httpContext ) ;
1971
+
1972
+ Assert . True ( invoked ) ;
1973
+ }
1974
+
1672
1975
public static IEnumerable < object ? [ ] > ServiceParamOptionalityData
1673
1976
{
1674
1977
get
@@ -1843,6 +2146,16 @@ private class Todo : ITodo
1843
2146
public bool IsComplete { get ; set ; }
1844
2147
}
1845
2148
2149
+ private class CustomTodo : Todo
2150
+ {
2151
+ public static async ValueTask < object ? > BindAsync ( HttpContext context )
2152
+ {
2153
+ var body = await context . Request . ReadFromJsonAsync < CustomTodo > ( ) ;
2154
+ context . Request . Body . Position = 0 ;
2155
+ return body ;
2156
+ }
2157
+ }
2158
+
1846
2159
private record struct TodoStruct ( int Id , string ? Name , bool IsComplete ) : ITodo ;
1847
2160
1848
2161
private interface ITodo
0 commit comments